Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/plot/gmtpy.py: 77%
1646 statements
« prev ^ index » next coverage.py v6.5.0, created at 2024-02-05 09:37 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2024-02-05 09:37 +0000
1# http://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
5'''
6A Python interface to GMT.
7'''
9# This file has been part of GmtPy (http://emolch.github.io/gmtpy/)
10# See there for copying and licensing information.
12import subprocess
13try:
14 from StringIO import StringIO as BytesIO
15except ImportError:
16 from io import BytesIO
17import re
18import os
19import sys
20import shutil
21from os.path import join as pjoin
22import tempfile
23import random
24import logging
25import math
26import numpy as num
27import copy
28from select import select
29try:
30 from scipy.io import netcdf_file
31except ImportError:
32 from scipy.io.netcdf import netcdf_file
34from pyrocko import ExternalProgramMissing
35from . import AutoScaler
38find_bb = re.compile(br'%%BoundingBox:((\s+[-0-9]+){4})')
39find_hiresbb = re.compile(br'%%HiResBoundingBox:((\s+[-0-9.]+){4})')
42encoding_gmt_to_python = {
43 'isolatin1+': 'iso-8859-1',
44 'standard+': 'ascii',
45 'isolatin1': 'iso-8859-1',
46 'standard': 'ascii'}
48for i in range(1, 11):
49 encoding_gmt_to_python['iso-8859-%i' % i] = 'iso-8859-%i' % i
52def have_gmt():
53 try:
54 get_gmt_installation('newest')
55 return True
57 except GMTInstallationProblem:
58 return False
61def check_have_gmt():
62 if not have_gmt():
63 raise ExternalProgramMissing('GMT is not installed or cannot be found')
66def have_pixmaptools():
67 for prog in [['pdftocairo'], ['convert'], ['gs', '-h']]:
68 try:
69 p = subprocess.Popen(
70 prog,
71 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
73 (stdout, stderr) = p.communicate()
75 except OSError:
76 return False
78 return True
81class GmtPyError(Exception):
82 pass
85class GMTError(GmtPyError):
86 pass
89class GMTInstallationProblem(GmtPyError):
90 pass
93def convert_graph(in_filename, out_filename, resolution=75., oversample=2.,
94 width=None, height=None, size=None):
96 _, tmp_filename_base = tempfile.mkstemp()
98 try:
99 if out_filename.endswith('.svg'):
100 fmt_arg = '-svg'
101 tmp_filename = tmp_filename_base
102 oversample = 1.0
103 else:
104 fmt_arg = '-png'
105 tmp_filename = tmp_filename_base + '-1.png'
107 if size is not None:
108 scale_args = ['-scale-to', '%i' % int(round(size*oversample))]
109 elif width is not None:
110 scale_args = ['-scale-to-x', '%i' % int(round(width*oversample))]
111 elif height is not None:
112 scale_args = ['-scale-to-y', '%i' % int(round(height*oversample))]
113 else:
114 scale_args = ['-r', '%i' % int(round(resolution * oversample))]
116 try:
117 subprocess.check_call(
118 ['pdftocairo'] + scale_args +
119 [fmt_arg, in_filename, tmp_filename_base])
120 except OSError as e:
121 raise GmtPyError(
122 'Cannot start `pdftocairo`, is it installed? (%s)' % str(e))
124 if oversample > 1.:
125 try:
126 subprocess.check_call([
127 'convert',
128 tmp_filename,
129 '-resize', '%i%%' % int(round(100.0/oversample)),
130 out_filename])
131 except OSError as e:
132 raise GmtPyError(
133 'Cannot start `convert`, is it installed? (%s)' % str(e))
135 else:
136 if out_filename.endswith('.png') or out_filename.endswith('.svg'):
137 shutil.move(tmp_filename, out_filename)
138 else:
139 try:
140 subprocess.check_call(
141 ['convert', tmp_filename, out_filename])
142 except Exception as e:
143 raise GmtPyError(
144 'Cannot start `convert`, is it installed? (%s)'
145 % str(e))
147 except Exception:
148 raise
150 finally:
151 if os.path.exists(tmp_filename_base):
152 os.remove(tmp_filename_base)
154 if os.path.exists(tmp_filename):
155 os.remove(tmp_filename)
158def get_bbox(s):
159 for pat in [find_hiresbb, find_bb]:
160 m = pat.search(s)
161 if m:
162 bb = [float(x) for x in m.group(1).split()]
163 return bb
165 raise GmtPyError('Cannot find bbox')
168def replace_bbox(bbox, *args):
170 def repl(m):
171 if m.group(1):
172 return ('%%HiResBoundingBox: ' + ' '.join(
173 '%.3f' % float(x) for x in bbox)).encode('ascii')
174 else:
175 return ('%%%%BoundingBox: %i %i %i %i' % (
176 int(math.floor(bbox[0])),
177 int(math.floor(bbox[1])),
178 int(math.ceil(bbox[2])),
179 int(math.ceil(bbox[3])))).encode('ascii')
181 pat = re.compile(br'%%(HiRes)?BoundingBox:((\s+[0-9.]+){4})')
182 if len(args) == 1:
183 s = args[0]
184 return pat.sub(repl, s)
186 else:
187 fin, fout = args
188 nn = 0
189 for line in fin:
190 line, n = pat.subn(repl, line)
191 nn += n
192 fout.write(line)
193 if nn == 2:
194 break
196 if nn == 2:
197 for line in fin:
198 fout.write(line)
201def escape_shell_arg(s):
202 '''
203 This function should be used for debugging output only - it could be
204 insecure.
205 '''
207 if re.search(r'[^a-zA-Z0-9._/=-]', s):
208 return "'" + s.replace("'", "'\\''") + "'"
209 else:
210 return s
213def escape_shell_args(args):
214 '''
215 This function should be used for debugging output only - it could be
216 insecure.
217 '''
219 return ' '.join([escape_shell_arg(x) for x in args])
222golden_ratio = 1.61803
224# units in points
225_units = {
226 'i': 72.,
227 'c': 72./2.54,
228 'm': 72.*100./2.54,
229 'p': 1.}
231inch = _units['i']
232'''
233Unit cm in points.
234'''
237cm = _units['c']
238'''
239Unit inch in points.
240'''
242# some awsome colors
243tango_colors = {
244 'butter1': (252, 233, 79),
245 'butter2': (237, 212, 0),
246 'butter3': (196, 160, 0),
247 'chameleon1': (138, 226, 52),
248 'chameleon2': (115, 210, 22),
249 'chameleon3': (78, 154, 6),
250 'orange1': (252, 175, 62),
251 'orange2': (245, 121, 0),
252 'orange3': (206, 92, 0),
253 'skyblue1': (114, 159, 207),
254 'skyblue2': (52, 101, 164),
255 'skyblue3': (32, 74, 135),
256 'plum1': (173, 127, 168),
257 'plum2': (117, 80, 123),
258 'plum3': (92, 53, 102),
259 'chocolate1': (233, 185, 110),
260 'chocolate2': (193, 125, 17),
261 'chocolate3': (143, 89, 2),
262 'scarletred1': (239, 41, 41),
263 'scarletred2': (204, 0, 0),
264 'scarletred3': (164, 0, 0),
265 'aluminium1': (238, 238, 236),
266 'aluminium2': (211, 215, 207),
267 'aluminium3': (186, 189, 182),
268 'aluminium4': (136, 138, 133),
269 'aluminium5': (85, 87, 83),
270 'aluminium6': (46, 52, 54)
271}
273graph_colors = [tango_colors[_x] for _x in (
274 'scarletred2', 'skyblue3', 'chameleon3', 'orange2', 'plum2', 'chocolate2',
275 'butter2')]
278def color(x=None):
279 '''
280 Generate a string for GMT option arguments expecting a color.
282 If ``x`` is None, a random color is returned. If it is an integer, the
283 corresponding ``gmtpy.graph_colors[x]`` or black returned. If it is a
284 string and the corresponding ``gmtpy.tango_colors[x]`` exists, this is
285 returned, or the string is passed through. If ``x`` is a tuple, it is
286 transformed into the string form which GMT expects.
287 '''
289 if x is None:
290 return '%i/%i/%i' % tuple(random.randint(0, 255) for _ in 'rgb')
292 if isinstance(x, int):
293 if 0 <= x < len(graph_colors):
294 return '%i/%i/%i' % graph_colors[x]
295 else:
296 return '0/0/0'
298 elif isinstance(x, str):
299 if x in tango_colors:
300 return '%i/%i/%i' % tango_colors[x]
301 else:
302 return x
304 return '%i/%i/%i' % x
307def color_tup(x=None):
308 if x is None:
309 return tuple([random.randint(0, 255) for _x in 'rgb'])
311 if isinstance(x, int):
312 if 0 <= x < len(graph_colors):
313 return graph_colors[x]
314 else:
315 return (0, 0, 0)
317 elif isinstance(x, str):
318 if x in tango_colors:
319 return tango_colors[x]
321 return x
324_gmt_installations = {}
326# Set fixed installation(s) to use...
327# (use this, if you want to use different GMT versions simultaneously.)
329# _gmt_installations['4.2.1'] = {'home': '/sw/etch-ia32/gmt-4.2.1',
330# 'bin': '/sw/etch-ia32/gmt-4.2.1/bin'}
331# _gmt_installations['4.3.0'] = {'home': '/sw/etch-ia32/gmt-4.3.0',
332# 'bin': '/sw/etch-ia32/gmt-4.3.0/bin'}
333# _gmt_installations['6.0.0'] = {'home': '/usr/share/gmt',
334# 'bin': '/usr/bin' }
336# ... or let GmtPy autodetect GMT via $PATH and $GMTHOME
339def key_version(a):
340 a = a.split('_')[0] # get rid of revision id
341 return [int(x) for x in a.split('.')]
344def newest_installed_gmt_version():
345 return sorted(_gmt_installations.keys(), key=key_version)[-1]
348def all_installed_gmt_versions():
349 return sorted(_gmt_installations.keys(), key=key_version)
352# To have consistent defaults, they are hardcoded here and should not be
353# changed.
355_gmt_defaults_by_version = {}
356_gmt_defaults_by_version['4.2.1'] = r'''
357#
358# GMT-SYSTEM 4.2.1 Defaults file
359#
360#-------- Plot Media Parameters -------------
361PAGE_COLOR = 255/255/255
362PAGE_ORIENTATION = portrait
363PAPER_MEDIA = a4+
364#-------- Basemap Annotation Parameters ------
365ANNOT_MIN_ANGLE = 20
366ANNOT_MIN_SPACING = 0
367ANNOT_FONT_PRIMARY = Helvetica
368ANNOT_FONT_SIZE = 12p
369ANNOT_OFFSET_PRIMARY = 0.075i
370ANNOT_FONT_SECONDARY = Helvetica
371ANNOT_FONT_SIZE_SECONDARY = 16p
372ANNOT_OFFSET_SECONDARY = 0.075i
373DEGREE_SYMBOL = ring
374HEADER_FONT = Helvetica
375HEADER_FONT_SIZE = 36p
376HEADER_OFFSET = 0.1875i
377LABEL_FONT = Helvetica
378LABEL_FONT_SIZE = 14p
379LABEL_OFFSET = 0.1125i
380OBLIQUE_ANNOTATION = 1
381PLOT_CLOCK_FORMAT = hh:mm:ss
382PLOT_DATE_FORMAT = yyyy-mm-dd
383PLOT_DEGREE_FORMAT = +ddd:mm:ss
384Y_AXIS_TYPE = hor_text
385#-------- Basemap Layout Parameters ---------
386BASEMAP_AXES = WESN
387BASEMAP_FRAME_RGB = 0/0/0
388BASEMAP_TYPE = plain
389FRAME_PEN = 1.25p
390FRAME_WIDTH = 0.075i
391GRID_CROSS_SIZE_PRIMARY = 0i
392GRID_CROSS_SIZE_SECONDARY = 0i
393GRID_PEN_PRIMARY = 0.25p
394GRID_PEN_SECONDARY = 0.5p
395MAP_SCALE_HEIGHT = 0.075i
396TICK_LENGTH = 0.075i
397POLAR_CAP = 85/90
398TICK_PEN = 0.5p
399X_AXIS_LENGTH = 9i
400Y_AXIS_LENGTH = 6i
401X_ORIGIN = 1i
402Y_ORIGIN = 1i
403UNIX_TIME = FALSE
404UNIX_TIME_POS = -0.75i/-0.75i
405#-------- Color System Parameters -----------
406COLOR_BACKGROUND = 0/0/0
407COLOR_FOREGROUND = 255/255/255
408COLOR_NAN = 128/128/128
409COLOR_IMAGE = adobe
410COLOR_MODEL = rgb
411HSV_MIN_SATURATION = 1
412HSV_MAX_SATURATION = 0.1
413HSV_MIN_VALUE = 0.3
414HSV_MAX_VALUE = 1
415#-------- PostScript Parameters -------------
416CHAR_ENCODING = ISOLatin1+
417DOTS_PR_INCH = 300
418N_COPIES = 1
419PS_COLOR = rgb
420PS_IMAGE_COMPRESS = none
421PS_IMAGE_FORMAT = ascii
422PS_LINE_CAP = round
423PS_LINE_JOIN = miter
424PS_MITER_LIMIT = 35
425PS_VERBOSE = FALSE
426GLOBAL_X_SCALE = 1
427GLOBAL_Y_SCALE = 1
428#-------- I/O Format Parameters -------------
429D_FORMAT = %lg
430FIELD_DELIMITER = tab
431GRIDFILE_SHORTHAND = FALSE
432GRID_FORMAT = nf
433INPUT_CLOCK_FORMAT = hh:mm:ss
434INPUT_DATE_FORMAT = yyyy-mm-dd
435IO_HEADER = FALSE
436N_HEADER_RECS = 1
437OUTPUT_CLOCK_FORMAT = hh:mm:ss
438OUTPUT_DATE_FORMAT = yyyy-mm-dd
439OUTPUT_DEGREE_FORMAT = +D
440XY_TOGGLE = FALSE
441#-------- Projection Parameters -------------
442ELLIPSOID = WGS-84
443MAP_SCALE_FACTOR = default
444MEASURE_UNIT = inch
445#-------- Calendar/Time Parameters ----------
446TIME_FORMAT_PRIMARY = full
447TIME_FORMAT_SECONDARY = full
448TIME_EPOCH = 2000-01-01T00:00:00
449TIME_IS_INTERVAL = OFF
450TIME_INTERVAL_FRACTION = 0.5
451TIME_LANGUAGE = us
452TIME_SYSTEM = other
453TIME_UNIT = d
454TIME_WEEK_START = Sunday
455Y2K_OFFSET_YEAR = 1950
456#-------- Miscellaneous Parameters ----------
457HISTORY = TRUE
458INTERPOLANT = akima
459LINE_STEP = 0.01i
460VECTOR_SHAPE = 0
461VERBOSE = FALSE'''
463_gmt_defaults_by_version['4.3.0'] = r'''
464#
465# GMT-SYSTEM 4.3.0 Defaults file
466#
467#-------- Plot Media Parameters -------------
468PAGE_COLOR = 255/255/255
469PAGE_ORIENTATION = portrait
470PAPER_MEDIA = a4+
471#-------- Basemap Annotation Parameters ------
472ANNOT_MIN_ANGLE = 20
473ANNOT_MIN_SPACING = 0
474ANNOT_FONT_PRIMARY = Helvetica
475ANNOT_FONT_SIZE_PRIMARY = 12p
476ANNOT_OFFSET_PRIMARY = 0.075i
477ANNOT_FONT_SECONDARY = Helvetica
478ANNOT_FONT_SIZE_SECONDARY = 16p
479ANNOT_OFFSET_SECONDARY = 0.075i
480DEGREE_SYMBOL = ring
481HEADER_FONT = Helvetica
482HEADER_FONT_SIZE = 36p
483HEADER_OFFSET = 0.1875i
484LABEL_FONT = Helvetica
485LABEL_FONT_SIZE = 14p
486LABEL_OFFSET = 0.1125i
487OBLIQUE_ANNOTATION = 1
488PLOT_CLOCK_FORMAT = hh:mm:ss
489PLOT_DATE_FORMAT = yyyy-mm-dd
490PLOT_DEGREE_FORMAT = +ddd:mm:ss
491Y_AXIS_TYPE = hor_text
492#-------- Basemap Layout Parameters ---------
493BASEMAP_AXES = WESN
494BASEMAP_FRAME_RGB = 0/0/0
495BASEMAP_TYPE = plain
496FRAME_PEN = 1.25p
497FRAME_WIDTH = 0.075i
498GRID_CROSS_SIZE_PRIMARY = 0i
499GRID_PEN_PRIMARY = 0.25p
500GRID_CROSS_SIZE_SECONDARY = 0i
501GRID_PEN_SECONDARY = 0.5p
502MAP_SCALE_HEIGHT = 0.075i
503POLAR_CAP = 85/90
504TICK_LENGTH = 0.075i
505TICK_PEN = 0.5p
506X_AXIS_LENGTH = 9i
507Y_AXIS_LENGTH = 6i
508X_ORIGIN = 1i
509Y_ORIGIN = 1i
510UNIX_TIME = FALSE
511UNIX_TIME_POS = BL/-0.75i/-0.75i
512UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
513#-------- Color System Parameters -----------
514COLOR_BACKGROUND = 0/0/0
515COLOR_FOREGROUND = 255/255/255
516COLOR_NAN = 128/128/128
517COLOR_IMAGE = adobe
518COLOR_MODEL = rgb
519HSV_MIN_SATURATION = 1
520HSV_MAX_SATURATION = 0.1
521HSV_MIN_VALUE = 0.3
522HSV_MAX_VALUE = 1
523#-------- PostScript Parameters -------------
524CHAR_ENCODING = ISOLatin1+
525DOTS_PR_INCH = 300
526N_COPIES = 1
527PS_COLOR = rgb
528PS_IMAGE_COMPRESS = none
529PS_IMAGE_FORMAT = ascii
530PS_LINE_CAP = round
531PS_LINE_JOIN = miter
532PS_MITER_LIMIT = 35
533PS_VERBOSE = FALSE
534GLOBAL_X_SCALE = 1
535GLOBAL_Y_SCALE = 1
536#-------- I/O Format Parameters -------------
537D_FORMAT = %lg
538FIELD_DELIMITER = tab
539GRIDFILE_SHORTHAND = FALSE
540GRID_FORMAT = nf
541INPUT_CLOCK_FORMAT = hh:mm:ss
542INPUT_DATE_FORMAT = yyyy-mm-dd
543IO_HEADER = FALSE
544N_HEADER_RECS = 1
545OUTPUT_CLOCK_FORMAT = hh:mm:ss
546OUTPUT_DATE_FORMAT = yyyy-mm-dd
547OUTPUT_DEGREE_FORMAT = +D
548XY_TOGGLE = FALSE
549#-------- Projection Parameters -------------
550ELLIPSOID = WGS-84
551MAP_SCALE_FACTOR = default
552MEASURE_UNIT = inch
553#-------- Calendar/Time Parameters ----------
554TIME_FORMAT_PRIMARY = full
555TIME_FORMAT_SECONDARY = full
556TIME_EPOCH = 2000-01-01T00:00:00
557TIME_IS_INTERVAL = OFF
558TIME_INTERVAL_FRACTION = 0.5
559TIME_LANGUAGE = us
560TIME_UNIT = d
561TIME_WEEK_START = Sunday
562Y2K_OFFSET_YEAR = 1950
563#-------- Miscellaneous Parameters ----------
564HISTORY = TRUE
565INTERPOLANT = akima
566LINE_STEP = 0.01i
567VECTOR_SHAPE = 0
568VERBOSE = FALSE'''
571_gmt_defaults_by_version['4.3.1'] = r'''
572#
573# GMT-SYSTEM 4.3.1 Defaults file
574#
575#-------- Plot Media Parameters -------------
576PAGE_COLOR = 255/255/255
577PAGE_ORIENTATION = portrait
578PAPER_MEDIA = a4+
579#-------- Basemap Annotation Parameters ------
580ANNOT_MIN_ANGLE = 20
581ANNOT_MIN_SPACING = 0
582ANNOT_FONT_PRIMARY = Helvetica
583ANNOT_FONT_SIZE_PRIMARY = 12p
584ANNOT_OFFSET_PRIMARY = 0.075i
585ANNOT_FONT_SECONDARY = Helvetica
586ANNOT_FONT_SIZE_SECONDARY = 16p
587ANNOT_OFFSET_SECONDARY = 0.075i
588DEGREE_SYMBOL = ring
589HEADER_FONT = Helvetica
590HEADER_FONT_SIZE = 36p
591HEADER_OFFSET = 0.1875i
592LABEL_FONT = Helvetica
593LABEL_FONT_SIZE = 14p
594LABEL_OFFSET = 0.1125i
595OBLIQUE_ANNOTATION = 1
596PLOT_CLOCK_FORMAT = hh:mm:ss
597PLOT_DATE_FORMAT = yyyy-mm-dd
598PLOT_DEGREE_FORMAT = +ddd:mm:ss
599Y_AXIS_TYPE = hor_text
600#-------- Basemap Layout Parameters ---------
601BASEMAP_AXES = WESN
602BASEMAP_FRAME_RGB = 0/0/0
603BASEMAP_TYPE = plain
604FRAME_PEN = 1.25p
605FRAME_WIDTH = 0.075i
606GRID_CROSS_SIZE_PRIMARY = 0i
607GRID_PEN_PRIMARY = 0.25p
608GRID_CROSS_SIZE_SECONDARY = 0i
609GRID_PEN_SECONDARY = 0.5p
610MAP_SCALE_HEIGHT = 0.075i
611POLAR_CAP = 85/90
612TICK_LENGTH = 0.075i
613TICK_PEN = 0.5p
614X_AXIS_LENGTH = 9i
615Y_AXIS_LENGTH = 6i
616X_ORIGIN = 1i
617Y_ORIGIN = 1i
618UNIX_TIME = FALSE
619UNIX_TIME_POS = BL/-0.75i/-0.75i
620UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
621#-------- Color System Parameters -----------
622COLOR_BACKGROUND = 0/0/0
623COLOR_FOREGROUND = 255/255/255
624COLOR_NAN = 128/128/128
625COLOR_IMAGE = adobe
626COLOR_MODEL = rgb
627HSV_MIN_SATURATION = 1
628HSV_MAX_SATURATION = 0.1
629HSV_MIN_VALUE = 0.3
630HSV_MAX_VALUE = 1
631#-------- PostScript Parameters -------------
632CHAR_ENCODING = ISOLatin1+
633DOTS_PR_INCH = 300
634N_COPIES = 1
635PS_COLOR = rgb
636PS_IMAGE_COMPRESS = none
637PS_IMAGE_FORMAT = ascii
638PS_LINE_CAP = round
639PS_LINE_JOIN = miter
640PS_MITER_LIMIT = 35
641PS_VERBOSE = FALSE
642GLOBAL_X_SCALE = 1
643GLOBAL_Y_SCALE = 1
644#-------- I/O Format Parameters -------------
645D_FORMAT = %lg
646FIELD_DELIMITER = tab
647GRIDFILE_SHORTHAND = FALSE
648GRID_FORMAT = nf
649INPUT_CLOCK_FORMAT = hh:mm:ss
650INPUT_DATE_FORMAT = yyyy-mm-dd
651IO_HEADER = FALSE
652N_HEADER_RECS = 1
653OUTPUT_CLOCK_FORMAT = hh:mm:ss
654OUTPUT_DATE_FORMAT = yyyy-mm-dd
655OUTPUT_DEGREE_FORMAT = +D
656XY_TOGGLE = FALSE
657#-------- Projection Parameters -------------
658ELLIPSOID = WGS-84
659MAP_SCALE_FACTOR = default
660MEASURE_UNIT = inch
661#-------- Calendar/Time Parameters ----------
662TIME_FORMAT_PRIMARY = full
663TIME_FORMAT_SECONDARY = full
664TIME_EPOCH = 2000-01-01T00:00:00
665TIME_IS_INTERVAL = OFF
666TIME_INTERVAL_FRACTION = 0.5
667TIME_LANGUAGE = us
668TIME_UNIT = d
669TIME_WEEK_START = Sunday
670Y2K_OFFSET_YEAR = 1950
671#-------- Miscellaneous Parameters ----------
672HISTORY = TRUE
673INTERPOLANT = akima
674LINE_STEP = 0.01i
675VECTOR_SHAPE = 0
676VERBOSE = FALSE'''
679_gmt_defaults_by_version['4.4.0'] = r'''
680#
681# GMT-SYSTEM 4.4.0 [64-bit] Defaults file
682#
683#-------- Plot Media Parameters -------------
684PAGE_COLOR = 255/255/255
685PAGE_ORIENTATION = portrait
686PAPER_MEDIA = a4+
687#-------- Basemap Annotation Parameters ------
688ANNOT_MIN_ANGLE = 20
689ANNOT_MIN_SPACING = 0
690ANNOT_FONT_PRIMARY = Helvetica
691ANNOT_FONT_SIZE_PRIMARY = 14p
692ANNOT_OFFSET_PRIMARY = 0.075i
693ANNOT_FONT_SECONDARY = Helvetica
694ANNOT_FONT_SIZE_SECONDARY = 16p
695ANNOT_OFFSET_SECONDARY = 0.075i
696DEGREE_SYMBOL = ring
697HEADER_FONT = Helvetica
698HEADER_FONT_SIZE = 36p
699HEADER_OFFSET = 0.1875i
700LABEL_FONT = Helvetica
701LABEL_FONT_SIZE = 14p
702LABEL_OFFSET = 0.1125i
703OBLIQUE_ANNOTATION = 1
704PLOT_CLOCK_FORMAT = hh:mm:ss
705PLOT_DATE_FORMAT = yyyy-mm-dd
706PLOT_DEGREE_FORMAT = +ddd:mm:ss
707Y_AXIS_TYPE = hor_text
708#-------- Basemap Layout Parameters ---------
709BASEMAP_AXES = WESN
710BASEMAP_FRAME_RGB = 0/0/0
711BASEMAP_TYPE = plain
712FRAME_PEN = 1.25p
713FRAME_WIDTH = 0.075i
714GRID_CROSS_SIZE_PRIMARY = 0i
715GRID_PEN_PRIMARY = 0.25p
716GRID_CROSS_SIZE_SECONDARY = 0i
717GRID_PEN_SECONDARY = 0.5p
718MAP_SCALE_HEIGHT = 0.075i
719POLAR_CAP = 85/90
720TICK_LENGTH = 0.075i
721TICK_PEN = 0.5p
722X_AXIS_LENGTH = 9i
723Y_AXIS_LENGTH = 6i
724X_ORIGIN = 1i
725Y_ORIGIN = 1i
726UNIX_TIME = FALSE
727UNIX_TIME_POS = BL/-0.75i/-0.75i
728UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
729#-------- Color System Parameters -----------
730COLOR_BACKGROUND = 0/0/0
731COLOR_FOREGROUND = 255/255/255
732COLOR_NAN = 128/128/128
733COLOR_IMAGE = adobe
734COLOR_MODEL = rgb
735HSV_MIN_SATURATION = 1
736HSV_MAX_SATURATION = 0.1
737HSV_MIN_VALUE = 0.3
738HSV_MAX_VALUE = 1
739#-------- PostScript Parameters -------------
740CHAR_ENCODING = ISOLatin1+
741DOTS_PR_INCH = 300
742N_COPIES = 1
743PS_COLOR = rgb
744PS_IMAGE_COMPRESS = lzw
745PS_IMAGE_FORMAT = ascii
746PS_LINE_CAP = round
747PS_LINE_JOIN = miter
748PS_MITER_LIMIT = 35
749PS_VERBOSE = FALSE
750GLOBAL_X_SCALE = 1
751GLOBAL_Y_SCALE = 1
752#-------- I/O Format Parameters -------------
753D_FORMAT = %lg
754FIELD_DELIMITER = tab
755GRIDFILE_SHORTHAND = FALSE
756GRID_FORMAT = nf
757INPUT_CLOCK_FORMAT = hh:mm:ss
758INPUT_DATE_FORMAT = yyyy-mm-dd
759IO_HEADER = FALSE
760N_HEADER_RECS = 1
761OUTPUT_CLOCK_FORMAT = hh:mm:ss
762OUTPUT_DATE_FORMAT = yyyy-mm-dd
763OUTPUT_DEGREE_FORMAT = +D
764XY_TOGGLE = FALSE
765#-------- Projection Parameters -------------
766ELLIPSOID = WGS-84
767MAP_SCALE_FACTOR = default
768MEASURE_UNIT = inch
769#-------- Calendar/Time Parameters ----------
770TIME_FORMAT_PRIMARY = full
771TIME_FORMAT_SECONDARY = full
772TIME_EPOCH = 2000-01-01T00:00:00
773TIME_IS_INTERVAL = OFF
774TIME_INTERVAL_FRACTION = 0.5
775TIME_LANGUAGE = us
776TIME_UNIT = d
777TIME_WEEK_START = Sunday
778Y2K_OFFSET_YEAR = 1950
779#-------- Miscellaneous Parameters ----------
780HISTORY = TRUE
781INTERPOLANT = akima
782LINE_STEP = 0.01i
783VECTOR_SHAPE = 0
784VERBOSE = FALSE
785'''
787_gmt_defaults_by_version['4.5.2'] = r'''
788#
789# GMT-SYSTEM 4.5.2 [64-bit] Defaults file
790#
791#-------- Plot Media Parameters -------------
792PAGE_COLOR = white
793PAGE_ORIENTATION = portrait
794PAPER_MEDIA = a4+
795#-------- Basemap Annotation Parameters ------
796ANNOT_MIN_ANGLE = 20
797ANNOT_MIN_SPACING = 0
798ANNOT_FONT_PRIMARY = Helvetica
799ANNOT_FONT_SIZE_PRIMARY = 14p
800ANNOT_OFFSET_PRIMARY = 0.075i
801ANNOT_FONT_SECONDARY = Helvetica
802ANNOT_FONT_SIZE_SECONDARY = 16p
803ANNOT_OFFSET_SECONDARY = 0.075i
804DEGREE_SYMBOL = ring
805HEADER_FONT = Helvetica
806HEADER_FONT_SIZE = 36p
807HEADER_OFFSET = 0.1875i
808LABEL_FONT = Helvetica
809LABEL_FONT_SIZE = 14p
810LABEL_OFFSET = 0.1125i
811OBLIQUE_ANNOTATION = 1
812PLOT_CLOCK_FORMAT = hh:mm:ss
813PLOT_DATE_FORMAT = yyyy-mm-dd
814PLOT_DEGREE_FORMAT = +ddd:mm:ss
815Y_AXIS_TYPE = hor_text
816#-------- Basemap Layout Parameters ---------
817BASEMAP_AXES = WESN
818BASEMAP_FRAME_RGB = black
819BASEMAP_TYPE = plain
820FRAME_PEN = 1.25p
821FRAME_WIDTH = 0.075i
822GRID_CROSS_SIZE_PRIMARY = 0i
823GRID_PEN_PRIMARY = 0.25p
824GRID_CROSS_SIZE_SECONDARY = 0i
825GRID_PEN_SECONDARY = 0.5p
826MAP_SCALE_HEIGHT = 0.075i
827POLAR_CAP = 85/90
828TICK_LENGTH = 0.075i
829TICK_PEN = 0.5p
830X_AXIS_LENGTH = 9i
831Y_AXIS_LENGTH = 6i
832X_ORIGIN = 1i
833Y_ORIGIN = 1i
834UNIX_TIME = FALSE
835UNIX_TIME_POS = BL/-0.75i/-0.75i
836UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
837#-------- Color System Parameters -----------
838COLOR_BACKGROUND = black
839COLOR_FOREGROUND = white
840COLOR_NAN = 128
841COLOR_IMAGE = adobe
842COLOR_MODEL = rgb
843HSV_MIN_SATURATION = 1
844HSV_MAX_SATURATION = 0.1
845HSV_MIN_VALUE = 0.3
846HSV_MAX_VALUE = 1
847#-------- PostScript Parameters -------------
848CHAR_ENCODING = ISOLatin1+
849DOTS_PR_INCH = 300
850GLOBAL_X_SCALE = 1
851GLOBAL_Y_SCALE = 1
852N_COPIES = 1
853PS_COLOR = rgb
854PS_IMAGE_COMPRESS = lzw
855PS_IMAGE_FORMAT = ascii
856PS_LINE_CAP = round
857PS_LINE_JOIN = miter
858PS_MITER_LIMIT = 35
859PS_VERBOSE = FALSE
860TRANSPARENCY = 0
861#-------- I/O Format Parameters -------------
862D_FORMAT = %.12lg
863FIELD_DELIMITER = tab
864GRIDFILE_FORMAT = nf
865GRIDFILE_SHORTHAND = FALSE
866INPUT_CLOCK_FORMAT = hh:mm:ss
867INPUT_DATE_FORMAT = yyyy-mm-dd
868IO_HEADER = FALSE
869N_HEADER_RECS = 1
870NAN_RECORDS = pass
871OUTPUT_CLOCK_FORMAT = hh:mm:ss
872OUTPUT_DATE_FORMAT = yyyy-mm-dd
873OUTPUT_DEGREE_FORMAT = D
874XY_TOGGLE = FALSE
875#-------- Projection Parameters -------------
876ELLIPSOID = WGS-84
877MAP_SCALE_FACTOR = default
878MEASURE_UNIT = inch
879#-------- Calendar/Time Parameters ----------
880TIME_FORMAT_PRIMARY = full
881TIME_FORMAT_SECONDARY = full
882TIME_EPOCH = 2000-01-01T00:00:00
883TIME_IS_INTERVAL = OFF
884TIME_INTERVAL_FRACTION = 0.5
885TIME_LANGUAGE = us
886TIME_UNIT = d
887TIME_WEEK_START = Sunday
888Y2K_OFFSET_YEAR = 1950
889#-------- Miscellaneous Parameters ----------
890HISTORY = TRUE
891INTERPOLANT = akima
892LINE_STEP = 0.01i
893VECTOR_SHAPE = 0
894VERBOSE = FALSE
895'''
897_gmt_defaults_by_version['4.5.3'] = r'''
898#
899# GMT-SYSTEM 4.5.3 (CVS Jun 18 2010 10:56:07) [64-bit] Defaults file
900#
901#-------- Plot Media Parameters -------------
902PAGE_COLOR = white
903PAGE_ORIENTATION = portrait
904PAPER_MEDIA = a4+
905#-------- Basemap Annotation Parameters ------
906ANNOT_MIN_ANGLE = 20
907ANNOT_MIN_SPACING = 0
908ANNOT_FONT_PRIMARY = Helvetica
909ANNOT_FONT_SIZE_PRIMARY = 14p
910ANNOT_OFFSET_PRIMARY = 0.075i
911ANNOT_FONT_SECONDARY = Helvetica
912ANNOT_FONT_SIZE_SECONDARY = 16p
913ANNOT_OFFSET_SECONDARY = 0.075i
914DEGREE_SYMBOL = ring
915HEADER_FONT = Helvetica
916HEADER_FONT_SIZE = 36p
917HEADER_OFFSET = 0.1875i
918LABEL_FONT = Helvetica
919LABEL_FONT_SIZE = 14p
920LABEL_OFFSET = 0.1125i
921OBLIQUE_ANNOTATION = 1
922PLOT_CLOCK_FORMAT = hh:mm:ss
923PLOT_DATE_FORMAT = yyyy-mm-dd
924PLOT_DEGREE_FORMAT = +ddd:mm:ss
925Y_AXIS_TYPE = hor_text
926#-------- Basemap Layout Parameters ---------
927BASEMAP_AXES = WESN
928BASEMAP_FRAME_RGB = black
929BASEMAP_TYPE = plain
930FRAME_PEN = 1.25p
931FRAME_WIDTH = 0.075i
932GRID_CROSS_SIZE_PRIMARY = 0i
933GRID_PEN_PRIMARY = 0.25p
934GRID_CROSS_SIZE_SECONDARY = 0i
935GRID_PEN_SECONDARY = 0.5p
936MAP_SCALE_HEIGHT = 0.075i
937POLAR_CAP = 85/90
938TICK_LENGTH = 0.075i
939TICK_PEN = 0.5p
940X_AXIS_LENGTH = 9i
941Y_AXIS_LENGTH = 6i
942X_ORIGIN = 1i
943Y_ORIGIN = 1i
944UNIX_TIME = FALSE
945UNIX_TIME_POS = BL/-0.75i/-0.75i
946UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
947#-------- Color System Parameters -----------
948COLOR_BACKGROUND = black
949COLOR_FOREGROUND = white
950COLOR_NAN = 128
951COLOR_IMAGE = adobe
952COLOR_MODEL = rgb
953HSV_MIN_SATURATION = 1
954HSV_MAX_SATURATION = 0.1
955HSV_MIN_VALUE = 0.3
956HSV_MAX_VALUE = 1
957#-------- PostScript Parameters -------------
958CHAR_ENCODING = ISOLatin1+
959DOTS_PR_INCH = 300
960GLOBAL_X_SCALE = 1
961GLOBAL_Y_SCALE = 1
962N_COPIES = 1
963PS_COLOR = rgb
964PS_IMAGE_COMPRESS = lzw
965PS_IMAGE_FORMAT = ascii
966PS_LINE_CAP = round
967PS_LINE_JOIN = miter
968PS_MITER_LIMIT = 35
969PS_VERBOSE = FALSE
970TRANSPARENCY = 0
971#-------- I/O Format Parameters -------------
972D_FORMAT = %.12lg
973FIELD_DELIMITER = tab
974GRIDFILE_FORMAT = nf
975GRIDFILE_SHORTHAND = FALSE
976INPUT_CLOCK_FORMAT = hh:mm:ss
977INPUT_DATE_FORMAT = yyyy-mm-dd
978IO_HEADER = FALSE
979N_HEADER_RECS = 1
980NAN_RECORDS = pass
981OUTPUT_CLOCK_FORMAT = hh:mm:ss
982OUTPUT_DATE_FORMAT = yyyy-mm-dd
983OUTPUT_DEGREE_FORMAT = D
984XY_TOGGLE = FALSE
985#-------- Projection Parameters -------------
986ELLIPSOID = WGS-84
987MAP_SCALE_FACTOR = default
988MEASURE_UNIT = inch
989#-------- Calendar/Time Parameters ----------
990TIME_FORMAT_PRIMARY = full
991TIME_FORMAT_SECONDARY = full
992TIME_EPOCH = 2000-01-01T00:00:00
993TIME_IS_INTERVAL = OFF
994TIME_INTERVAL_FRACTION = 0.5
995TIME_LANGUAGE = us
996TIME_UNIT = d
997TIME_WEEK_START = Sunday
998Y2K_OFFSET_YEAR = 1950
999#-------- Miscellaneous Parameters ----------
1000HISTORY = TRUE
1001INTERPOLANT = akima
1002LINE_STEP = 0.01i
1003VECTOR_SHAPE = 0
1004VERBOSE = FALSE
1005'''
1007_gmt_defaults_by_version['5.1.2'] = r'''
1008#
1009# GMT 5.1.2 Defaults file
1010# vim:sw=8:ts=8:sts=8
1011# $Revision: 13836 $
1012# $LastChangedDate: 2014-12-20 03:45:42 -1000 (Sat, 20 Dec 2014) $
1013#
1014# COLOR Parameters
1015#
1016COLOR_BACKGROUND = black
1017COLOR_FOREGROUND = white
1018COLOR_NAN = 127.5
1019COLOR_MODEL = none
1020COLOR_HSV_MIN_S = 1
1021COLOR_HSV_MAX_S = 0.1
1022COLOR_HSV_MIN_V = 0.3
1023COLOR_HSV_MAX_V = 1
1024#
1025# DIR Parameters
1026#
1027DIR_DATA =
1028DIR_DCW =
1029DIR_GSHHG =
1030#
1031# FONT Parameters
1032#
1033FONT_ANNOT_PRIMARY = 14p,Helvetica,black
1034FONT_ANNOT_SECONDARY = 16p,Helvetica,black
1035FONT_LABEL = 14p,Helvetica,black
1036FONT_LOGO = 8p,Helvetica,black
1037FONT_TITLE = 24p,Helvetica,black
1038#
1039# FORMAT Parameters
1040#
1041FORMAT_CLOCK_IN = hh:mm:ss
1042FORMAT_CLOCK_OUT = hh:mm:ss
1043FORMAT_CLOCK_MAP = hh:mm:ss
1044FORMAT_DATE_IN = yyyy-mm-dd
1045FORMAT_DATE_OUT = yyyy-mm-dd
1046FORMAT_DATE_MAP = yyyy-mm-dd
1047FORMAT_GEO_OUT = D
1048FORMAT_GEO_MAP = ddd:mm:ss
1049FORMAT_FLOAT_OUT = %.12g
1050FORMAT_FLOAT_MAP = %.12g
1051FORMAT_TIME_PRIMARY_MAP = full
1052FORMAT_TIME_SECONDARY_MAP = full
1053FORMAT_TIME_STAMP = %Y %b %d %H:%M:%S
1054#
1055# GMT Miscellaneous Parameters
1056#
1057GMT_COMPATIBILITY = 4
1058GMT_CUSTOM_LIBS =
1059GMT_EXTRAPOLATE_VAL = NaN
1060GMT_FFT = auto
1061GMT_HISTORY = true
1062GMT_INTERPOLANT = akima
1063GMT_TRIANGULATE = Shewchuk
1064GMT_VERBOSE = compat
1065GMT_LANGUAGE = us
1066#
1067# I/O Parameters
1068#
1069IO_COL_SEPARATOR = tab
1070IO_GRIDFILE_FORMAT = nf
1071IO_GRIDFILE_SHORTHAND = false
1072IO_HEADER = false
1073IO_N_HEADER_RECS = 0
1074IO_NAN_RECORDS = pass
1075IO_NC4_CHUNK_SIZE = auto
1076IO_NC4_DEFLATION_LEVEL = 3
1077IO_LONLAT_TOGGLE = false
1078IO_SEGMENT_MARKER = >
1079#
1080# MAP Parameters
1081#
1082MAP_ANNOT_MIN_ANGLE = 20
1083MAP_ANNOT_MIN_SPACING = 0p
1084MAP_ANNOT_OBLIQUE = 1
1085MAP_ANNOT_OFFSET_PRIMARY = 0.075i
1086MAP_ANNOT_OFFSET_SECONDARY = 0.075i
1087MAP_ANNOT_ORTHO = we
1088MAP_DEFAULT_PEN = default,black
1089MAP_DEGREE_SYMBOL = ring
1090MAP_FRAME_AXES = WESNZ
1091MAP_FRAME_PEN = thicker,black
1092MAP_FRAME_TYPE = fancy
1093MAP_FRAME_WIDTH = 5p
1094MAP_GRID_CROSS_SIZE_PRIMARY = 0p
1095MAP_GRID_CROSS_SIZE_SECONDARY = 0p
1096MAP_GRID_PEN_PRIMARY = default,black
1097MAP_GRID_PEN_SECONDARY = thinner,black
1098MAP_LABEL_OFFSET = 0.1944i
1099MAP_LINE_STEP = 0.75p
1100MAP_LOGO = false
1101MAP_LOGO_POS = BL/-54p/-54p
1102MAP_ORIGIN_X = 1i
1103MAP_ORIGIN_Y = 1i
1104MAP_POLAR_CAP = 85/90
1105MAP_SCALE_HEIGHT = 5p
1106MAP_TICK_LENGTH_PRIMARY = 5p/2.5p
1107MAP_TICK_LENGTH_SECONDARY = 15p/3.75p
1108MAP_TICK_PEN_PRIMARY = thinner,black
1109MAP_TICK_PEN_SECONDARY = thinner,black
1110MAP_TITLE_OFFSET = 14p
1111MAP_VECTOR_SHAPE = 0
1112#
1113# Projection Parameters
1114#
1115PROJ_AUX_LATITUDE = authalic
1116PROJ_ELLIPSOID = WGS-84
1117PROJ_LENGTH_UNIT = cm
1118PROJ_MEAN_RADIUS = authalic
1119PROJ_SCALE_FACTOR = default
1120#
1121# PostScript Parameters
1122#
1123PS_CHAR_ENCODING = ISOLatin1+
1124PS_COLOR_MODEL = rgb
1125PS_COMMENTS = false
1126PS_IMAGE_COMPRESS = deflate,5
1127PS_LINE_CAP = butt
1128PS_LINE_JOIN = miter
1129PS_MITER_LIMIT = 35
1130PS_MEDIA = a4
1131PS_PAGE_COLOR = white
1132PS_PAGE_ORIENTATION = portrait
1133PS_SCALE_X = 1
1134PS_SCALE_Y = 1
1135PS_TRANSPARENCY = Normal
1136#
1137# Calendar/Time Parameters
1138#
1139TIME_EPOCH = 1970-01-01T00:00:00
1140TIME_IS_INTERVAL = off
1141TIME_INTERVAL_FRACTION = 0.5
1142TIME_UNIT = s
1143TIME_WEEK_START = Monday
1144TIME_Y2K_OFFSET_YEAR = 1950
1145'''
1148def get_gmt_version(gmtdefaultsbinary, gmthomedir=None):
1149 args = [gmtdefaultsbinary]
1151 environ = os.environ.copy()
1152 environ['GMTHOME'] = gmthomedir or ''
1154 p = subprocess.Popen(
1155 args,
1156 stdout=subprocess.PIPE,
1157 stderr=subprocess.PIPE,
1158 env=environ)
1160 (stdout, stderr) = p.communicate()
1161 m = re.search(br'(\d+(\.\d+)*)', stderr) \
1162 or re.search(br'# GMT (\d+(\.\d+)*)', stdout)
1164 if not m:
1165 raise GMTInstallationProblem(
1166 "Can't extract version number from output of %s."
1167 % gmtdefaultsbinary)
1169 return str(m.group(1).decode('ascii'))
1172def detect_gmt_installations():
1174 installations = {}
1175 errmesses = []
1177 # GMT 4.x:
1178 try:
1179 p = subprocess.Popen(
1180 ['GMT'],
1181 stdout=subprocess.PIPE,
1182 stderr=subprocess.PIPE)
1184 (stdout, stderr) = p.communicate()
1186 m = re.search(br'Version\s+(\d+(\.\d+)*)', stderr, re.M)
1187 if not m:
1188 raise GMTInstallationProblem(
1189 "Can't get version number from output of GMT.")
1191 version = str(m.group(1).decode('ascii'))
1192 if version[0] != '5':
1194 m = re.search(br'^\s+executables\s+(.+)$', stderr, re.M)
1195 if not m:
1196 raise GMTInstallationProblem(
1197 "Can't extract executables dir from output of GMT.")
1199 gmtbin = str(m.group(1).decode('ascii'))
1201 m = re.search(br'^\s+shared data\s+(.+)$', stderr, re.M)
1202 if not m:
1203 raise GMTInstallationProblem(
1204 "Can't extract shared dir from output of GMT.")
1206 gmtshare = str(m.group(1).decode('ascii'))
1207 if not gmtshare.endswith('/share'):
1208 raise GMTInstallationProblem(
1209 "Can't determine GMTHOME from output of GMT.")
1211 gmthome = gmtshare[:-6]
1213 installations[version] = {
1214 'home': gmthome,
1215 'bin': gmtbin}
1217 except OSError as e:
1218 errmesses.append(('GMT', str(e)))
1220 try:
1221 version = str(subprocess.check_output(
1222 ['gmt', '--version']).strip().decode('ascii')).split('_')[0]
1223 gmtbin = str(subprocess.check_output(
1224 ['gmt', '--show-bindir']).strip().decode('ascii'))
1225 installations[version] = {
1226 'bin': gmtbin}
1228 except (OSError, subprocess.CalledProcessError) as e:
1229 errmesses.append(('gmt', str(e)))
1231 if not installations:
1232 s = []
1233 for (progname, errmess) in errmesses:
1234 s.append('Cannot start "%s" executable: %s' % (progname, errmess))
1236 raise GMTInstallationProblem(', '.join(s))
1238 return installations
1241def appropriate_defaults_version(version):
1242 avails = sorted(_gmt_defaults_by_version.keys(), key=key_version)
1243 for iavail, avail in enumerate(avails):
1244 if key_version(version) == key_version(avail):
1245 return version
1247 elif key_version(version) < key_version(avail):
1248 return avails[max(0, iavail-1)]
1250 return avails[-1]
1253def gmt_default_config(version):
1254 '''
1255 Get default GMT configuration dict for given version.
1256 '''
1258 xversion = appropriate_defaults_version(version)
1260 # if not version in _gmt_defaults_by_version:
1261 # raise GMTError('No GMT defaults for version %s found' % version)
1263 gmt_defaults = _gmt_defaults_by_version[xversion]
1265 d = {}
1266 for line in gmt_defaults.splitlines():
1267 sline = line.strip()
1268 if not sline or sline.startswith('#'):
1269 continue
1271 k, v = sline.split('=', 1)
1272 d[k.strip()] = v.strip()
1274 return d
1277def diff_defaults(v1, v2):
1278 d1 = gmt_default_config(v1)
1279 d2 = gmt_default_config(v2)
1280 for k in d1:
1281 if k not in d2:
1282 print('%s not in %s' % (k, v2))
1283 else:
1284 if d1[k] != d2[k]:
1285 print('%s %s = %s' % (v1, k, d1[k]))
1286 print('%s %s = %s' % (v2, k, d2[k]))
1288 for k in d2:
1289 if k not in d1:
1290 print('%s not in %s' % (k, v1))
1292# diff_defaults('4.5.2', '4.5.3')
1295def check_gmt_installation(installation):
1297 home_dir = installation.get('home', None)
1298 bin_dir = installation['bin']
1299 version = installation['version']
1301 for d in home_dir, bin_dir:
1302 if d is not None:
1303 if not os.path.exists(d):
1304 logging.error(('Directory does not exist: %s\n'
1305 'Check your GMT installation.') % d)
1307 major_version = version.split('.')[0]
1309 if major_version not in ['5', '6']:
1310 gmtdefaults = pjoin(bin_dir, 'gmtdefaults')
1312 versionfound = get_gmt_version(gmtdefaults, home_dir)
1314 if versionfound != version:
1315 raise GMTInstallationProblem((
1316 'Expected GMT version %s but found version %s.\n'
1317 '(Looking at output of %s)') % (
1318 version, versionfound, gmtdefaults))
1321def get_gmt_installation(version):
1322 setup_gmt_installations()
1323 if version != 'newest' and version not in _gmt_installations:
1324 logging.warn('GMT version %s not installed, taking version %s instead'
1325 % (version, newest_installed_gmt_version()))
1327 version = 'newest'
1329 if version == 'newest':
1330 version = newest_installed_gmt_version()
1332 installation = dict(_gmt_installations[version])
1334 return installation
1337def setup_gmt_installations():
1338 if not setup_gmt_installations.have_done:
1339 if not _gmt_installations:
1341 _gmt_installations.update(detect_gmt_installations())
1343 # store defaults as dicts into the gmt installations dicts
1344 for version, installation in _gmt_installations.items():
1345 installation['defaults'] = gmt_default_config(version)
1346 installation['version'] = version
1348 for installation in _gmt_installations.values():
1349 check_gmt_installation(installation)
1351 setup_gmt_installations.have_done = True
1354setup_gmt_installations.have_done = False
1356_paper_sizes_a = '''A0 2380 3368
1357 A1 1684 2380
1358 A2 1190 1684
1359 A3 842 1190
1360 A4 595 842
1361 A5 421 595
1362 A6 297 421
1363 A7 210 297
1364 A8 148 210
1365 A9 105 148
1366 A10 74 105
1367 B0 2836 4008
1368 B1 2004 2836
1369 B2 1418 2004
1370 B3 1002 1418
1371 B4 709 1002
1372 B5 501 709
1373 archA 648 864
1374 archB 864 1296
1375 archC 1296 1728
1376 archD 1728 2592
1377 archE 2592 3456
1378 flsa 612 936
1379 halfletter 396 612
1380 note 540 720
1381 letter 612 792
1382 legal 612 1008
1383 11x17 792 1224
1384 ledger 1224 792'''
1387_paper_sizes = {}
1390def setup_paper_sizes():
1391 if not _paper_sizes:
1392 for line in _paper_sizes_a.splitlines():
1393 k, w, h = line.split()
1394 _paper_sizes[k.lower()] = float(w), float(h)
1397def get_paper_size(k):
1398 setup_paper_sizes()
1399 return _paper_sizes[k.lower().rstrip('+')]
1402def all_paper_sizes():
1403 setup_paper_sizes()
1404 return _paper_sizes
1407def measure_unit(gmt_config):
1408 for k in ['MEASURE_UNIT', 'PROJ_LENGTH_UNIT']:
1409 if k in gmt_config:
1410 return gmt_config[k]
1412 raise GmtPyError('cannot get measure unit / proj length unit from config')
1415def paper_media(gmt_config):
1416 for k in ['PAPER_MEDIA', 'PS_MEDIA']:
1417 if k in gmt_config:
1418 return gmt_config[k]
1420 raise GmtPyError('cannot get paper media from config')
1423def page_orientation(gmt_config):
1424 for k in ['PAGE_ORIENTATION', 'PS_PAGE_ORIENTATION']:
1425 if k in gmt_config:
1426 return gmt_config[k]
1428 raise GmtPyError('cannot get paper orientation from config')
1431def make_bbox(width, height, gmt_config, margins=(0.8, 0.8, 0.8, 0.8)):
1433 leftmargin, topmargin, rightmargin, bottommargin = margins
1434 portrait = page_orientation(gmt_config).lower() == 'portrait'
1436 paper_size = get_paper_size(paper_media(gmt_config))
1437 if not portrait:
1438 paper_size = paper_size[1], paper_size[0]
1440 xoffset = (paper_size[0] - (width + leftmargin + rightmargin)) / \
1441 2.0 + leftmargin
1442 yoffset = (paper_size[1] - (height + topmargin + bottommargin)) / \
1443 2.0 + bottommargin
1445 if portrait:
1446 bb1 = int((xoffset - leftmargin))
1447 bb2 = int((yoffset - bottommargin))
1448 bb3 = bb1 + int((width+leftmargin+rightmargin))
1449 bb4 = bb2 + int((height+topmargin+bottommargin))
1450 else:
1451 bb1 = int((yoffset - topmargin))
1452 bb2 = int((xoffset - leftmargin))
1453 bb3 = bb1 + int((height+topmargin+bottommargin))
1454 bb4 = bb2 + int((width+leftmargin+rightmargin))
1456 return xoffset, yoffset, (bb1, bb2, bb3, bb4)
1459def gmtdefaults_as_text(version='newest'):
1461 '''
1462 Get the built-in gmtdefaults.
1463 '''
1465 if version not in _gmt_installations:
1466 logging.warn('GMT version %s not installed, taking version %s instead'
1467 % (version, newest_installed_gmt_version()))
1468 version = 'newest'
1470 if version == 'newest':
1471 version = newest_installed_gmt_version()
1473 return _gmt_defaults_by_version[version]
1476def savegrd(x, y, z, filename, title=None, naming='xy'):
1477 '''
1478 Write COARDS compliant netcdf (grd) file.
1479 '''
1481 assert y.size, x.size == z.shape
1482 ny, nx = z.shape
1483 nc = netcdf_file(filename, 'w')
1484 assert naming in ('xy', 'lonlat')
1486 if naming == 'xy':
1487 kx, ky = 'x', 'y'
1488 else:
1489 kx, ky = 'lon', 'lat'
1491 nc.node_offset = 0
1492 if title is not None:
1493 nc.title = title
1495 nc.Conventions = 'COARDS/CF-1.0'
1496 nc.createDimension(kx, nx)
1497 nc.createDimension(ky, ny)
1499 xvar = nc.createVariable(kx, 'd', (kx,))
1500 yvar = nc.createVariable(ky, 'd', (ky,))
1501 if naming == 'xy':
1502 xvar.long_name = kx
1503 yvar.long_name = ky
1504 else:
1505 xvar.long_name = 'longitude'
1506 xvar.units = 'degrees_east'
1507 yvar.long_name = 'latitude'
1508 yvar.units = 'degrees_north'
1510 zvar = nc.createVariable('z', 'd', (ky, kx))
1512 xvar[:] = x.astype(num.float64)
1513 yvar[:] = y.astype(num.float64)
1514 zvar[:] = z.astype(num.float64)
1516 nc.close()
1519def to_array(var):
1520 arr = var[:].copy()
1521 if hasattr(var, 'scale_factor'):
1522 arr = arr * var.scale_factor
1524 if hasattr(var, 'add_offset'):
1525 arr = arr + var.add_offset
1527 return arr
1530def loadgrd(filename):
1531 '''
1532 Read COARDS compliant netcdf (grd) file.
1533 '''
1535 from pyrocko import util
1537 nc = netcdf_file(filename, 'r')
1538 vkeys = list(nc.variables.keys())
1539 kx = 'x'
1540 ky = 'y'
1541 if 'lon' in vkeys:
1542 kx = 'lon'
1543 if 'lat' in vkeys:
1544 ky = 'lat'
1546 kz = 'z'
1547 if 'altitude' in vkeys:
1548 kz = 'altitude'
1550 try:
1551 if all(k in vkeys for k in ['x_range', 'y_range', 'spacing']):
1552 xmin, xmax = to_array(nc.variables['x_range'])
1553 ymin, ymax = to_array(nc.variables['y_range'])
1554 xdelta, ydelta = to_array(nc.variables['spacing'])
1555 x = util.arange2(xmin, xmax, xdelta)
1556 y = util.arange2(ymin, ymax, ydelta)
1558 else:
1559 x = to_array(nc.variables[kx])
1560 y = to_array(nc.variables[ky])
1562 z = to_array(nc.variables[kz])
1563 if z.ndim == 1 and 'dimension' in vkeys:
1564 z = z.reshape((y.size, x.size))
1566 except KeyError as e:
1567 raise GmtPyError(
1568 'Variable not found (available: %s): %s' % (
1569 ', '.join(vkeys), str(e))) from e
1571 nc.close()
1572 return x, y, z
1575def centers_to_edges(asorted):
1576 return (asorted[1:] + asorted[:-1])/2.
1579def nvals(asorted):
1580 eps = (asorted[-1]-asorted[0])/asorted.size
1581 return num.sum(asorted[1:] - asorted[:-1] >= eps) + 1
1584def guess_vals(asorted):
1585 eps = (asorted[-1]-asorted[0])/asorted.size
1586 indis = num.nonzero(asorted[1:] - asorted[:-1] >= eps)[0]
1587 indis = num.concatenate((num.array([0]), indis+1,
1588 num.array([asorted.size])))
1589 asum = num.zeros(asorted.size+1)
1590 asum[1:] = num.cumsum(asorted)
1591 return (asum[indis[1:]] - asum[indis[:-1]]) / (indis[1:]-indis[:-1])
1594def blockmean(asorted, b):
1595 indis = num.nonzero(asorted[1:] - asorted[:-1])[0]
1596 indis = num.concatenate((num.array([0]), indis+1,
1597 num.array([asorted.size])))
1598 bsum = num.zeros(b.size+1)
1599 bsum[1:] = num.cumsum(b)
1600 return (
1601 asorted[indis[:-1]],
1602 (bsum[indis[1:]] - bsum[indis[:-1]]) / (indis[1:]-indis[:-1]))
1605def griddata_regular(x, y, z, xvals, yvals):
1606 nx, ny = xvals.size, yvals.size
1607 xindi = num.digitize(x, centers_to_edges(xvals))
1608 yindi = num.digitize(y, centers_to_edges(yvals))
1610 zindi = yindi*nx+xindi
1611 order = num.argsort(zindi)
1612 z = z[order]
1613 zindi = zindi[order]
1615 zindi, z = blockmean(zindi, z)
1616 znew = num.empty(nx*ny, dtype=float)
1617 znew[:] = num.nan
1618 znew[zindi] = z
1619 return znew.reshape(ny, nx)
1622def guess_field_size(x_sorted, y_sorted, z=None, mode=None):
1623 critical_fraction = 1./num.e - 0.014*3
1624 xs = x_sorted
1625 ys = y_sorted
1626 nxs, nys = nvals(xs), nvals(ys)
1627 if mode == 'nonrandom':
1628 return nxs, nys, 0
1629 elif xs.size == nxs*nys:
1630 # exact match
1631 return nxs, nys, 0
1632 elif nxs >= xs.size*critical_fraction and nys >= xs.size*critical_fraction:
1633 # possibly randomly sampled
1634 nxs = int(math.sqrt(xs.size))
1635 nys = nxs
1636 return nxs, nys, 2
1637 else:
1638 return nxs, nys, 1
1641def griddata_auto(x, y, z, mode=None):
1642 '''
1643 Grid tabular XYZ data by binning.
1645 This function does some extra work to guess the size of the grid. This
1646 should work fine if the input values are already defined on an rectilinear
1647 grid, even if data points are missing or duplicated. This routine also
1648 tries to detect a random distribution of input data and in that case
1649 creates a grid of size sqrt(N) x sqrt(N).
1651 The points do not have to be given in any particular order. Grid nodes
1652 without data are assigned the NaN value. If multiple data points map to the
1653 same grid node, their average is assigned to the grid node.
1654 '''
1656 x, y, z = [num.asarray(X) for X in (x, y, z)]
1657 assert x.size == y.size == z.size
1658 xs, ys = num.sort(x), num.sort(y)
1659 nx, ny, badness = guess_field_size(xs, ys, z, mode=mode)
1660 if badness <= 1:
1661 xf = guess_vals(xs)
1662 yf = guess_vals(ys)
1663 zf = griddata_regular(x, y, z, xf, yf)
1664 else:
1665 xf = num.linspace(xs[0], xs[-1], nx)
1666 yf = num.linspace(ys[0], ys[-1], ny)
1667 zf = griddata_regular(x, y, z, xf, yf)
1669 return xf, yf, zf
1672def tabledata(xf, yf, zf):
1673 assert yf.size, xf.size == zf.shape
1674 x = num.tile(xf, yf.size)
1675 y = num.repeat(yf, xf.size)
1676 z = zf.flatten()
1677 return x, y, z
1680def double1d(a):
1681 a2 = num.empty(a.size*2-1)
1682 a2[::2] = a
1683 a2[1::2] = (a[:-1] + a[1:])/2.
1684 return a2
1687def double2d(f):
1688 f2 = num.empty((f.shape[0]*2-1, f.shape[1]*2-1))
1689 f2[:, :] = num.nan
1690 f2[::2, ::2] = f
1691 f2[1::2, ::2] = (f[:-1, :] + f[1:, :])/2.
1692 f2[::2, 1::2] = (f[:, :-1] + f[:, 1:])/2.
1693 f2[1::2, 1::2] = (f[:-1, :-1] + f[1:, :-1] + f[:-1, 1:] + f[1:, 1:])/4.
1694 diag = f2[1::2, 1::2]
1695 diagA = (f[:-1, :-1] + f[1:, 1:]) / 2.
1696 diagB = (f[1:, :-1] + f[:-1, 1:]) / 2.
1697 f2[1::2, 1::2] = num.where(num.isnan(diag), diagA, diag)
1698 f2[1::2, 1::2] = num.where(num.isnan(diag), diagB, diag)
1699 return f2
1702def doublegrid(x, y, z):
1703 x2 = double1d(x)
1704 y2 = double1d(y)
1705 z2 = double2d(z)
1706 return x2, y2, z2
1709class Guru(object):
1710 '''
1711 Abstract base class providing template interpolation, accessible as
1712 attributes.
1714 Classes deriving from this one, have to implement a :py:meth:`get_params`
1715 method, which is called to get a dict to do ordinary
1716 ``"%(key)x"``-substitutions. The deriving class must also provide a dict
1717 with the templates.
1718 '''
1720 def __init__(self):
1721 self.templates = {}
1723 def get_params(self, ax_projection=False):
1724 '''
1725 To be implemented in subclasses.
1726 '''
1727 raise NotImplementedError
1729 def fill(self, templates, **kwargs):
1730 params = self.get_params(**kwargs)
1731 strings = [t % params for t in templates]
1732 return strings
1734 # hand through templates dict
1735 def __getitem__(self, template_name):
1736 return self.templates[template_name]
1738 def __setitem__(self, template_name, template):
1739 self.templates[template_name] = template
1741 def __contains__(self, template_name):
1742 return template_name in self.templates
1744 def __iter__(self):
1745 return iter(self.templates)
1747 def __len__(self):
1748 return len(self.templates)
1750 def __delitem__(self, template_name):
1751 del self.templates[template_name]
1753 def _simple_fill(self, template_names, **kwargs):
1754 templates = [self.templates[n] for n in template_names]
1755 return self.fill(templates, **kwargs)
1757 def __getattr__(self, template_names):
1758 if [n for n in template_names if n not in self.templates]:
1759 raise AttributeError(template_names)
1761 def f(**kwargs):
1762 return self._simple_fill(template_names, **kwargs)
1764 return f
1767class Ax(AutoScaler):
1768 '''
1769 Ax description with autoscaling capabilities.
1771 The ax is described by the :py:class:`~pyrocko.plot.AutoScaler`
1772 public attributes, plus the following additional attributes
1773 (with default values given in paranthesis):
1775 .. py:attribute:: label
1777 Ax label (without unit).
1779 .. py:attribute:: unit
1781 Physical unit of the data attached to this ax.
1783 .. py:attribute:: scaled_unit
1785 (see below)
1787 .. py:attribute:: scaled_unit_factor
1789 Scaled physical unit and factor between unit and scaled_unit so that
1791 unit = scaled_unit_factor x scaled_unit.
1793 (E.g. if unit is 'm' and data is in the range of nanometers, you may
1794 want to set the scaled_unit to 'nm' and the scaled_unit_factor to
1795 1e9.)
1797 .. py:attribute:: limits
1799 If defined, fix range of ax to limits=(min,max).
1801 .. py:attribute:: masking
1803 If true and if there is a limit on the ax, while calculating ranges,
1804 the data points are masked such that data points outside of this axes
1805 limits are not used to determine the range of another dependant ax.
1807 '''
1809 def __init__(self, label='', unit='', scaled_unit_factor=1.,
1810 scaled_unit='', limits=None, masking=True, **kwargs):
1812 AutoScaler.__init__(self, **kwargs)
1813 self.label = label
1814 self.unit = unit
1815 self.scaled_unit_factor = scaled_unit_factor
1816 self.scaled_unit = scaled_unit
1817 self.limits = limits
1818 self.masking = masking
1820 def label_str(self, exp, unit):
1821 '''
1822 Get label string including the unit and multiplier.
1823 '''
1825 slabel, sunit, sexp = '', '', ''
1826 if self.label:
1827 slabel = self.label
1829 if unit or exp != 0:
1830 if exp != 0:
1831 sexp = '\\327 10@+%i@+' % exp
1832 sunit = '[ %s %s ]' % (sexp, unit)
1833 else:
1834 sunit = '[ %s ]' % unit
1836 p = []
1837 if slabel:
1838 p.append(slabel)
1840 if sunit:
1841 p.append(sunit)
1843 return ' '.join(p)
1845 def make_params(self, data_range, ax_projection=False, override_mode=None,
1846 override_scaled_unit_factor=None):
1848 '''
1849 Get minimum, maximum, increment and label string for ax display.'
1851 Returns minimum, maximum, increment and label string including unit and
1852 multiplier for given data range.
1854 If ``ax_projection`` is True, values suitable to be displayed on the ax
1855 are returned, e.g. min, max and inc are returned in scaled units.
1856 Otherwise the values are returned in the original units, without any
1857 scaling applied.
1858 '''
1860 sf = self.scaled_unit_factor
1862 if override_scaled_unit_factor is not None:
1863 sf = override_scaled_unit_factor
1865 dr_scaled = [sf*x for x in data_range]
1867 mi, ma, inc = self.make_scale(dr_scaled, override_mode=override_mode)
1868 if self.inc is not None:
1869 inc = self.inc*sf
1871 if ax_projection:
1872 exp = self.make_exp(inc)
1873 if sf == 1. and override_scaled_unit_factor is None:
1874 unit = self.unit
1875 else:
1876 unit = self.scaled_unit
1877 label = self.label_str(exp, unit)
1878 return mi/10**exp, ma/10**exp, inc/10**exp, label
1879 else:
1880 label = self.label_str(0, self.unit)
1881 return mi/sf, ma/sf, inc/sf, label
1884class ScaleGuru(Guru):
1886 '''
1887 2D/3D autoscaling and ax annotation facility.
1889 Instances of this class provide automatic determination of plot ranges,
1890 tick increments and scaled annotations, as well as label/unit handling. It
1891 can in particular be used to automatically generate the -R and -B option
1892 arguments, which are required for most GMT commands.
1894 It extends the functionality of the :py:class:`Ax` and
1895 :py:class:`~pyrocko.plot.AutoScaler` classes at the level, where it can not
1896 be handled anymore by looking at a single dimension of the dataset's data,
1897 e.g.:
1899 * The ability to impose a fixed aspect ratio between two axes.
1901 * Recalculation of data range on non-limited axes, when there are
1902 limits imposed on other axes.
1904 '''
1906 def __init__(self, data_tuples=None, axes=None, aspect=None,
1907 percent_interval=None, copy_from=None):
1909 Guru.__init__(self)
1911 if copy_from:
1912 self.templates = copy.deepcopy(copy_from.templates)
1913 self.axes = copy.deepcopy(copy_from.axes)
1914 self.data_ranges = copy.deepcopy(copy_from.data_ranges)
1915 self.aspect = copy_from.aspect
1917 if percent_interval is not None:
1918 from scipy.stats import scoreatpercentile as scap
1920 self.templates = dict(
1921 R='-R%(xmin)g/%(xmax)g/%(ymin)g/%(ymax)g',
1922 B='-B%(xinc)g:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen',
1923 T='-T%(zmin)g/%(zmax)g/%(zinc)g')
1925 maxdim = 2
1926 if data_tuples:
1927 maxdim = max(maxdim, max([len(dt) for dt in data_tuples]))
1928 else:
1929 if axes:
1930 maxdim = len(axes)
1931 data_tuples = [([],) * maxdim]
1932 if axes is not None:
1933 self.axes = axes
1934 else:
1935 self.axes = [Ax() for i in range(maxdim)]
1937 # sophisticated data-range calculation
1938 data_ranges = [None] * maxdim
1939 for dt_ in data_tuples:
1940 dt = num.asarray(dt_)
1941 in_range = True
1942 for ax, x in zip(self.axes, dt):
1943 if ax.limits and ax.masking:
1944 ax_limits = list(ax.limits)
1945 if ax_limits[0] is None:
1946 ax_limits[0] = -num.inf
1947 if ax_limits[1] is None:
1948 ax_limits[1] = num.inf
1949 in_range = num.logical_and(
1950 in_range,
1951 num.logical_and(ax_limits[0] <= x, x <= ax_limits[1]))
1953 for i, ax, x in zip(range(maxdim), self.axes, dt):
1955 if not ax.limits or None in ax.limits:
1956 if len(x) >= 1:
1957 if in_range is not True:
1958 xmasked = num.where(in_range, x, num.NaN)
1959 if percent_interval is None:
1960 range_this = (
1961 num.nanmin(xmasked),
1962 num.nanmax(xmasked))
1963 else:
1964 xmasked_finite = num.compress(
1965 num.isfinite(xmasked), xmasked)
1966 range_this = (
1967 scap(xmasked_finite,
1968 (100.-percent_interval)/2.),
1969 scap(xmasked_finite,
1970 100.-(100.-percent_interval)/2.))
1971 else:
1972 if percent_interval is None:
1973 range_this = num.nanmin(x), num.nanmax(x)
1974 else:
1975 xmasked_finite = num.compress(
1976 num.isfinite(xmasked), xmasked)
1977 range_this = (
1978 scap(xmasked_finite,
1979 (100.-percent_interval)/2.),
1980 scap(xmasked_finite,
1981 100.-(100.-percent_interval)/2.))
1982 else:
1983 range_this = (0., 1.)
1985 if ax.limits:
1986 if ax.limits[0] is not None:
1987 range_this = ax.limits[0], max(ax.limits[0],
1988 range_this[1])
1990 if ax.limits[1] is not None:
1991 range_this = min(ax.limits[1],
1992 range_this[0]), ax.limits[1]
1994 else:
1995 range_this = ax.limits
1997 if data_ranges[i] is None and range_this[0] <= range_this[1]:
1998 data_ranges[i] = range_this
1999 else:
2000 mi, ma = range_this
2001 if data_ranges[i] is not None:
2002 mi = min(data_ranges[i][0], mi)
2003 ma = max(data_ranges[i][1], ma)
2005 data_ranges[i] = (mi, ma)
2007 for i in range(len(data_ranges)):
2008 if data_ranges[i] is None or not (
2009 num.isfinite(data_ranges[i][0])
2010 and num.isfinite(data_ranges[i][1])):
2012 data_ranges[i] = (0., 1.)
2014 self.data_ranges = data_ranges
2015 self.aspect = aspect
2017 def copy(self):
2018 return ScaleGuru(copy_from=self)
2020 def get_params(self, ax_projection=False):
2022 '''
2023 Get dict with output parameters.
2025 For each data dimension, ax minimum, maximum, increment and a label
2026 string (including unit and exponential factor) are determined. E.g. in
2027 for the first dimension the output dict will contain the keys
2028 ``'xmin'``, ``'xmax'``, ``'xinc'``, and ``'xlabel'``.
2030 Normally, values corresponding to the scaling of the raw data are
2031 produced, but if ``ax_projection`` is ``True``, values which are
2032 suitable to be printed on the axes are returned. This means that in the
2033 latter case, the :py:attr:`Ax.scaled_unit` and
2034 :py:attr:`Ax.scaled_unit_factor` attributes as set on the axes are
2035 respected and that a common 10^x factor is factored out and put to the
2036 label string.
2037 '''
2039 xmi, xma, xinc, xlabel = self.axes[0].make_params(
2040 self.data_ranges[0], ax_projection)
2041 ymi, yma, yinc, ylabel = self.axes[1].make_params(
2042 self.data_ranges[1], ax_projection)
2043 if len(self.axes) > 2:
2044 zmi, zma, zinc, zlabel = self.axes[2].make_params(
2045 self.data_ranges[2], ax_projection)
2047 # enforce certain aspect, if needed
2048 if self.aspect is not None:
2049 xwid = xma-xmi
2050 ywid = yma-ymi
2051 if ywid < xwid*self.aspect:
2052 ymi -= (xwid*self.aspect - ywid)*0.5
2053 yma += (xwid*self.aspect - ywid)*0.5
2054 ymi, yma, yinc, ylabel = self.axes[1].make_params(
2055 (ymi, yma), ax_projection, override_mode='off',
2056 override_scaled_unit_factor=1.)
2058 elif xwid < ywid/self.aspect:
2059 xmi -= (ywid/self.aspect - xwid)*0.5
2060 xma += (ywid/self.aspect - xwid)*0.5
2061 xmi, xma, xinc, xlabel = self.axes[0].make_params(
2062 (xmi, xma), ax_projection, override_mode='off',
2063 override_scaled_unit_factor=1.)
2065 params = dict(xmin=xmi, xmax=xma, xinc=xinc, xlabel=xlabel,
2066 ymin=ymi, ymax=yma, yinc=yinc, ylabel=ylabel)
2067 if len(self.axes) > 2:
2068 params.update(dict(zmin=zmi, zmax=zma, zinc=zinc, zlabel=zlabel))
2070 return params
2073class GumSpring(object):
2075 '''
2076 Sizing policy implementing a minimal size, plus a desire to grow.
2077 '''
2079 def __init__(self, minimal=None, grow=None):
2080 self.minimal = minimal
2081 if grow is None:
2082 if minimal is None:
2083 self.grow = 1.0
2084 else:
2085 self.grow = 0.0
2086 else:
2087 self.grow = grow
2088 self.value = 1.0
2090 def get_minimal(self):
2091 if self.minimal is not None:
2092 return self.minimal
2093 else:
2094 return 0.0
2096 def get_grow(self):
2097 return self.grow
2099 def set_value(self, value):
2100 self.value = value
2102 def get_value(self):
2103 return self.value
2106def distribute(sizes, grows, space):
2107 sizes = list(sizes)
2108 gsum = sum(grows)
2109 if gsum > 0.0:
2110 for i in range(len(sizes)):
2111 sizes[i] += space*grows[i]/gsum
2112 return sizes
2115class Widget(Guru):
2117 '''
2118 Base class of the gmtpy layout system.
2120 The Widget class provides the basic functionality for the nesting and
2121 placing of elements on the output page, and maintains the sizing policies
2122 of each element. Each of the layouts defined in gmtpy is itself a Widget.
2124 Sizing of the widget is controlled by :py:meth:`get_min_size` and
2125 :py:meth:`get_grow` which should be overloaded in derived classes. The
2126 basic behaviour of a Widget instance is to have a vertical and a horizontal
2127 minimum size which default to zero, as well as a vertical and a horizontal
2128 desire to grow, represented by floats, which default to 1.0. Additionally
2129 an aspect ratio constraint may be imposed on the Widget.
2131 After layouting, the widget provides its width, height, x-offset and
2132 y-offset in various ways. Via the Guru interface (see :py:class:`Guru`
2133 class), templates for the -X, -Y and -J option arguments used by GMT
2134 arguments are provided. The defaults are suitable for plotting of linear
2135 (-JX) plots. Other projections can be selected by giving an appropriate 'J'
2136 template, or by manual construction of the -J option, e.g. by utilizing the
2137 :py:meth:`width` and :py:meth:`height` methods. The :py:meth:`bbox` method
2138 can be used to create a PostScript bounding box from the widgets border,
2139 e.g. for use in :py:meth:`GMT.save`.
2141 The convention is, that all sizes are given in PostScript points.
2142 Conversion factors are provided as constants :py:const:`inch` and
2143 :py:const:`cm` in the gmtpy module.
2144 '''
2146 def __init__(self, horizontal=None, vertical=None, parent=None):
2148 '''
2149 Create new widget.
2150 '''
2152 Guru.__init__(self)
2154 self.templates = dict(
2155 X='-Xa%(xoffset)gp',
2156 Y='-Ya%(yoffset)gp',
2157 J='-JX%(width)gp/%(height)gp')
2159 if horizontal is None:
2160 self.horizontal = GumSpring()
2161 else:
2162 self.horizontal = horizontal
2164 if vertical is None:
2165 self.vertical = GumSpring()
2166 else:
2167 self.vertical = vertical
2169 self.aspect = None
2170 self.parent = parent
2171 self.dirty = True
2173 def set_widget(self):
2174 '''
2175 To be implemented in subclasses.
2176 '''
2177 raise NotImplementedError
2179 def set_parent(self, parent):
2181 '''
2182 Set the parent widget.
2184 This method should not be called directly. The :py:meth:`set_widget`
2185 methods are responsible for calling this.
2186 '''
2188 self.parent = parent
2189 self.dirtyfy()
2191 def get_parent(self):
2193 '''
2194 Get the widgets parent widget.
2195 '''
2197 return self.parent
2199 def get_root(self):
2201 '''
2202 Get the root widget in the layout hierarchy.
2203 '''
2205 if self.parent is not None:
2206 return self.get_parent()
2207 else:
2208 return self
2210 def set_horizontal(self, minimal=None, grow=None):
2212 '''
2213 Set the horizontal sizing policy of the Widget.
2216 :param minimal: new minimal width of the widget
2217 :param grow: new horizontal grow disire of the widget
2218 '''
2220 self.horizontal = GumSpring(minimal, grow)
2221 self.dirtyfy()
2223 def get_horizontal(self):
2224 return self.horizontal.get_minimal(), self.horizontal.get_grow()
2226 def set_vertical(self, minimal=None, grow=None):
2228 '''
2229 Set the horizontal sizing policy of the Widget.
2231 :param minimal: new minimal height of the widget
2232 :param grow: new vertical grow disire of the widget
2233 '''
2235 self.vertical = GumSpring(minimal, grow)
2236 self.dirtyfy()
2238 def get_vertical(self):
2239 return self.vertical.get_minimal(), self.vertical.get_grow()
2241 def set_aspect(self, aspect=None):
2243 '''
2244 Set aspect constraint on the widget.
2246 The aspect is given as height divided by width.
2247 '''
2249 self.aspect = aspect
2250 self.dirtyfy()
2252 def set_policy(self, minimal=(None, None), grow=(None, None), aspect=None):
2254 '''
2255 Shortcut to set sizing and aspect constraints in a single method
2256 call.
2257 '''
2259 self.set_horizontal(minimal[0], grow[0])
2260 self.set_vertical(minimal[1], grow[1])
2261 self.set_aspect(aspect)
2263 def get_policy(self):
2264 mh, gh = self.get_horizontal()
2265 mv, gv = self.get_vertical()
2266 return (mh, mv), (gh, gv), self.aspect
2268 def legalize(self, size, offset):
2270 '''
2271 Get legal size for widget.
2273 Returns: (new_size, new_offset)
2275 Given a box as ``size`` and ``offset``, return ``new_size`` and
2276 ``new_offset``, such that the widget's sizing and aspect constraints
2277 are fullfilled. The returned box is centered on the given input box.
2278 '''
2280 sh, sv = size
2281 oh, ov = offset
2282 shs, svs = Widget.get_min_size(self)
2283 ghs, gvs = Widget.get_grow(self)
2285 if ghs == 0.0:
2286 oh += (sh-shs)/2.
2287 sh = shs
2289 if gvs == 0.0:
2290 ov += (sv-svs)/2.
2291 sv = svs
2293 if self.aspect is not None:
2294 if sh > sv/self.aspect:
2295 oh += (sh-sv/self.aspect)/2.
2296 sh = sv/self.aspect
2297 if sv > sh*self.aspect:
2298 ov += (sv-sh*self.aspect)/2.
2299 sv = sh*self.aspect
2301 return (sh, sv), (oh, ov)
2303 def get_min_size(self):
2305 '''
2306 Get minimum size of widget.
2308 Used by the layout managers. Should be overloaded in derived classes.
2309 '''
2311 mh, mv = self.horizontal.get_minimal(), self.vertical.get_minimal()
2312 if self.aspect is not None:
2313 if mv == 0.0:
2314 return mh, mh*self.aspect
2315 elif mh == 0.0:
2316 return mv/self.aspect, mv
2317 return mh, mv
2319 def get_grow(self):
2321 '''
2322 Get widget's desire to grow.
2324 Used by the layout managers. Should be overloaded in derived classes.
2325 '''
2327 return self.horizontal.get_grow(), self.vertical.get_grow()
2329 def set_size(self, size, offset):
2331 '''
2332 Set the widget's current size.
2334 Should not be called directly. It is the layout manager's
2335 responsibility to call this.
2336 '''
2338 (sh, sv), inner_offset = self.legalize(size, offset)
2339 self.offset = inner_offset
2340 self.horizontal.set_value(sh)
2341 self.vertical.set_value(sv)
2342 self.dirty = False
2344 def __str__(self):
2346 def indent(ind, str):
2347 return ('\n'+ind).join(str.splitlines())
2348 size, offset = self.get_size()
2349 s = '%s (%g x %g) (%g, %g)\n' % ((self.__class__,) + size + offset)
2350 children = self.get_children()
2351 if children:
2352 s += '\n'.join([' ' + indent(' ', str(c)) for c in children])
2353 return s
2355 def policies_debug_str(self):
2357 def indent(ind, str):
2358 return ('\n'+ind).join(str.splitlines())
2359 mins, grows, aspect = self.get_policy()
2360 s = '%s: minimum=(%s, %s), grow=(%s, %s), aspect=%s\n' % (
2361 (self.__class__,) + mins+grows+(aspect,))
2363 children = self.get_children()
2364 if children:
2365 s += '\n'.join([' ' + indent(
2366 ' ', c.policies_debug_str()) for c in children])
2367 return s
2369 def get_corners(self, descend=False):
2371 '''
2372 Get coordinates of the corners of the widget.
2374 Returns list with coordinate tuples.
2376 If ``descend`` is True, the returned list will contain corner
2377 coordinates of all sub-widgets.
2378 '''
2380 self.do_layout()
2381 (sh, sv), (oh, ov) = self.get_size()
2382 corners = [(oh, ov), (oh+sh, ov), (oh+sh, ov+sv), (oh, ov+sv)]
2383 if descend:
2384 for child in self.get_children():
2385 corners.extend(child.get_corners(descend=True))
2386 return corners
2388 def get_sizes(self):
2390 '''
2391 Get sizes of this widget and all it's children.
2393 Returns a list with size tuples.
2394 '''
2395 self.do_layout()
2396 sizes = [self.get_size()]
2397 for child in self.get_children():
2398 sizes.extend(child.get_sizes())
2399 return sizes
2401 def do_layout(self):
2403 '''
2404 Triggers layouting of the widget hierarchy, if needed.
2405 '''
2407 if self.parent is not None:
2408 return self.parent.do_layout()
2410 if not self.dirty:
2411 return
2413 sh, sv = self.get_min_size()
2414 gh, gv = self.get_grow()
2415 if sh == 0.0 and gh != 0.0:
2416 sh = 15.*cm
2417 if sv == 0.0 and gv != 0.0:
2418 sv = 15.*cm*gv/gh * 1./golden_ratio
2419 self.set_size((sh, sv), (0., 0.))
2421 def get_children(self):
2423 '''
2424 Get sub-widgets contained in this widget.
2426 Returns a list of widgets.
2427 '''
2429 return []
2431 def get_size(self):
2433 '''
2434 Get current size and position of the widget.
2436 Triggers layouting and returns
2437 ``((width, height), (xoffset, yoffset))``
2438 '''
2440 self.do_layout()
2441 return (self.horizontal.get_value(),
2442 self.vertical.get_value()), self.offset
2444 def get_params(self):
2446 '''
2447 Get current size and position of the widget.
2449 Triggers layouting and returns dict with keys ``'xoffset'``,
2450 ``'yoffset'``, ``'width'`` and ``'height'``.
2451 '''
2453 self.do_layout()
2454 (w, h), (xo, yo) = self.get_size()
2455 return dict(xoffset=xo, yoffset=yo, width=w, height=h,
2456 width_m=w/_units['m'])
2458 def width(self):
2460 '''
2461 Get current width of the widget.
2463 Triggers layouting and returns width.
2464 '''
2466 self.do_layout()
2467 return self.horizontal.get_value()
2469 def height(self):
2471 '''
2472 Get current height of the widget.
2474 Triggers layouting and return height.
2475 '''
2477 self.do_layout()
2478 return self.vertical.get_value()
2480 def bbox(self):
2482 '''
2483 Get PostScript bounding box for this widget.
2485 Triggers layouting and returns values suitable to create PS bounding
2486 box, representing the widgets current size and position.
2487 '''
2489 self.do_layout()
2490 return (self.offset[0], self.offset[1], self.offset[0]+self.width(),
2491 self.offset[1]+self.height())
2493 def dirtyfy(self):
2495 '''
2496 Set dirty flag on top level widget in the hierarchy.
2498 Called by various methods, to indicate, that the widget hierarchy needs
2499 new layouting.
2500 '''
2502 if self.parent is not None:
2503 self.parent.dirtyfy()
2505 self.dirty = True
2508class CenterLayout(Widget):
2510 '''
2511 A layout manager which centers its single child widget.
2513 The child widget may be oversized.
2514 '''
2516 def __init__(self, horizontal=None, vertical=None):
2517 Widget.__init__(self, horizontal, vertical)
2518 self.content = Widget(horizontal=GumSpring(grow=1.),
2519 vertical=GumSpring(grow=1.), parent=self)
2521 def get_min_size(self):
2522 shs, svs = Widget.get_min_size(self)
2523 sh, sv = self.content.get_min_size()
2524 return max(shs, sh), max(svs, sv)
2526 def get_grow(self):
2527 ghs, gvs = Widget.get_grow(self)
2528 gh, gv = self.content.get_grow()
2529 return gh*ghs, gv*gvs
2531 def set_size(self, size, offset):
2532 (sh, sv), (oh, ov) = self.legalize(size, offset)
2534 shc, svc = self.content.get_min_size()
2535 ghc, gvc = self.content.get_grow()
2536 if ghc != 0.:
2537 shc = sh
2538 if gvc != 0.:
2539 svc = sv
2540 ohc = oh+(sh-shc)/2.
2541 ovc = ov+(sv-svc)/2.
2543 self.content.set_size((shc, svc), (ohc, ovc))
2544 Widget.set_size(self, (sh, sv), (oh, ov))
2546 def set_widget(self, widget=None):
2548 '''
2549 Set the child widget, which shall be centered.
2550 '''
2552 if widget is None:
2553 widget = Widget()
2555 self.content = widget
2557 widget.set_parent(self)
2559 def get_widget(self):
2560 return self.content
2562 def get_children(self):
2563 return [self.content]
2566class FrameLayout(Widget):
2568 '''
2569 A layout manager containing a center widget sorrounded by four margin
2570 widgets.
2572 ::
2574 +---------------------------+
2575 | top |
2576 +---------------------------+
2577 | | | |
2578 | left | center | right |
2579 | | | |
2580 +---------------------------+
2581 | bottom |
2582 +---------------------------+
2584 This layout manager does a little bit of extra effort to maintain the
2585 aspect constraint of the center widget, if this is set. It does so, by
2586 allowing for a bit more flexibility in the sizing of the margins. Two
2587 shortcut methods are provided to set the margin sizes in one shot:
2588 :py:meth:`set_fixed_margins` and :py:meth:`set_min_margins`. The first sets
2589 the margins to fixed sizes, while the second gives them a minimal size and
2590 a (neglectably) small desire to grow. Using the latter may be useful when
2591 setting an aspect constraint on the center widget, because this way the
2592 maximum size of the center widget may be controlled without creating empty
2593 spaces between the widgets.
2594 '''
2596 def __init__(self, horizontal=None, vertical=None):
2597 Widget.__init__(self, horizontal, vertical)
2598 mw = 3.*cm
2599 self.left = Widget(
2600 horizontal=GumSpring(grow=0.15, minimal=mw), parent=self)
2601 self.right = Widget(
2602 horizontal=GumSpring(grow=0.15, minimal=mw), parent=self)
2603 self.top = Widget(
2604 vertical=GumSpring(grow=0.15, minimal=mw/golden_ratio),
2605 parent=self)
2606 self.bottom = Widget(
2607 vertical=GumSpring(grow=0.15, minimal=mw/golden_ratio),
2608 parent=self)
2609 self.center = Widget(
2610 horizontal=GumSpring(grow=0.7), vertical=GumSpring(grow=0.7),
2611 parent=self)
2613 def set_fixed_margins(self, left, right, top, bottom):
2614 '''
2615 Give margins fixed size constraints.
2616 '''
2618 self.left.set_horizontal(left, 0)
2619 self.right.set_horizontal(right, 0)
2620 self.top.set_vertical(top, 0)
2621 self.bottom.set_vertical(bottom, 0)
2623 def set_min_margins(self, left, right, top, bottom, grow=0.0001):
2624 '''
2625 Give margins a minimal size and the possibility to grow.
2627 The desire to grow is set to a very small number.
2628 '''
2629 self.left.set_horizontal(left, grow)
2630 self.right.set_horizontal(right, grow)
2631 self.top.set_vertical(top, grow)
2632 self.bottom.set_vertical(bottom, grow)
2634 def get_min_size(self):
2635 shs, svs = Widget.get_min_size(self)
2637 sl, sr, st, sb, sc = [x.get_min_size() for x in (
2638 self.left, self.right, self.top, self.bottom, self.center)]
2639 gl, gr, gt, gb, gc = [x.get_grow() for x in (
2640 self.left, self.right, self.top, self.bottom, self.center)]
2642 shsum = sl[0]+sr[0]+sc[0]
2643 svsum = st[1]+sb[1]+sc[1]
2645 # prevent widgets from collapsing
2646 for s, g in ((sl, gl), (sr, gr), (sc, gc)):
2647 if s[0] == 0.0 and g[0] != 0.0:
2648 shsum += 0.1*cm
2650 for s, g in ((st, gt), (sb, gb), (sc, gc)):
2651 if s[1] == 0.0 and g[1] != 0.0:
2652 svsum += 0.1*cm
2654 sh = max(shs, shsum)
2655 sv = max(svs, svsum)
2657 return sh, sv
2659 def get_grow(self):
2660 ghs, gvs = Widget.get_grow(self)
2661 gh = (self.left.get_grow()[0] +
2662 self.right.get_grow()[0] +
2663 self.center.get_grow()[0]) * ghs
2664 gv = (self.top.get_grow()[1] +
2665 self.bottom.get_grow()[1] +
2666 self.center.get_grow()[1]) * gvs
2667 return gh, gv
2669 def set_size(self, size, offset):
2670 (sh, sv), (oh, ov) = self.legalize(size, offset)
2672 sl, sr, st, sb, sc = [x.get_min_size() for x in (
2673 self.left, self.right, self.top, self.bottom, self.center)]
2674 gl, gr, gt, gb, gc = [x.get_grow() for x in (
2675 self.left, self.right, self.top, self.bottom, self.center)]
2677 ah = sh - (sl[0]+sr[0]+sc[0])
2678 av = sv - (st[1]+sb[1]+sc[1])
2680 if ah < 0.0:
2681 raise GmtPyError('Container not wide enough for contents '
2682 '(FrameLayout, available: %g cm, needed: %g cm)'
2683 % (sh/cm, (sl[0]+sr[0]+sc[0])/cm))
2684 if av < 0.0:
2685 raise GmtPyError('Container not high enough for contents '
2686 '(FrameLayout, available: %g cm, needed: %g cm)'
2687 % (sv/cm, (st[1]+sb[1]+sc[1])/cm))
2689 slh, srh, sch = distribute((sl[0], sr[0], sc[0]),
2690 (gl[0], gr[0], gc[0]), ah)
2691 stv, sbv, scv = distribute((st[1], sb[1], sc[1]),
2692 (gt[1], gb[1], gc[1]), av)
2694 if self.center.aspect is not None:
2695 ahm = sh - (sl[0]+sr[0] + scv/self.center.aspect)
2696 avm = sv - (st[1]+sb[1] + sch*self.center.aspect)
2697 if 0.0 < ahm < ah:
2698 slh, srh, sch = distribute(
2699 (sl[0], sr[0], scv/self.center.aspect),
2700 (gl[0], gr[0], 0.0), ahm)
2702 elif 0.0 < avm < av:
2703 stv, sbv, scv = distribute((st[1], sb[1],
2704 sch*self.center.aspect),
2705 (gt[1], gb[1], 0.0), avm)
2707 ah = sh - (slh+srh+sch)
2708 av = sv - (stv+sbv+scv)
2710 oh += ah/2.
2711 ov += av/2.
2712 sh -= ah
2713 sv -= av
2715 self.left.set_size((slh, scv), (oh, ov+sbv))
2716 self.right.set_size((srh, scv), (oh+slh+sch, ov+sbv))
2717 self.top.set_size((sh, stv), (oh, ov+sbv+scv))
2718 self.bottom.set_size((sh, sbv), (oh, ov))
2719 self.center.set_size((sch, scv), (oh+slh, ov+sbv))
2720 Widget.set_size(self, (sh, sv), (oh, ov))
2722 def set_widget(self, which='center', widget=None):
2724 '''
2725 Set one of the sub-widgets.
2727 ``which`` should be one of ``'left'``, ``'right'``, ``'top'``,
2728 ``'bottom'`` or ``'center'``.
2729 '''
2731 if widget is None:
2732 widget = Widget()
2734 if which in ('left', 'right', 'top', 'bottom', 'center'):
2735 self.__dict__[which] = widget
2736 else:
2737 raise GmtPyError('No such sub-widget: %s' % which)
2739 widget.set_parent(self)
2741 def get_widget(self, which='center'):
2743 '''
2744 Get one of the sub-widgets.
2746 ``which`` should be one of ``'left'``, ``'right'``, ``'top'``,
2747 ``'bottom'`` or ``'center'``.
2748 '''
2750 if which in ('left', 'right', 'top', 'bottom', 'center'):
2751 return self.__dict__[which]
2752 else:
2753 raise GmtPyError('No such sub-widget: %s' % which)
2755 def get_children(self):
2756 return [self.left, self.right, self.top, self.bottom, self.center]
2759class GridLayout(Widget):
2761 '''
2762 A layout manager which arranges its sub-widgets in a grid.
2764 The grid spacing is flexible and based on the sizing policies of the
2765 contained sub-widgets. If an equidistant grid is needed, the sizing
2766 policies of the sub-widgets have to be set equally.
2768 The height of each row and the width of each column is derived from the
2769 sizing policy of the largest sub-widget in the row or column in question.
2770 The algorithm is not very sophisticated, so conflicting sizing policies
2771 might not be resolved optimally.
2772 '''
2774 def __init__(self, nx=2, ny=2, horizontal=None, vertical=None):
2776 '''
2777 Create new grid layout with ``nx`` columns and ``ny`` rows.
2778 '''
2780 Widget.__init__(self, horizontal, vertical)
2781 self.grid = []
2782 for iy in range(ny):
2783 row = []
2784 for ix in range(nx):
2785 w = Widget(parent=self)
2786 row.append(w)
2788 self.grid.append(row)
2790 def sub_min_sizes_as_array(self):
2791 esh = num.array(
2792 [[w.get_min_size()[0] for w in row] for row in self.grid],
2793 dtype=float)
2794 esv = num.array(
2795 [[w.get_min_size()[1] for w in row] for row in self.grid],
2796 dtype=float)
2797 return esh, esv
2799 def sub_grows_as_array(self):
2800 egh = num.array(
2801 [[w.get_grow()[0] for w in row] for row in self.grid],
2802 dtype=float)
2803 egv = num.array(
2804 [[w.get_grow()[1] for w in row] for row in self.grid],
2805 dtype=float)
2806 return egh, egv
2808 def get_min_size(self):
2809 sh, sv = Widget.get_min_size(self)
2810 esh, esv = self.sub_min_sizes_as_array()
2811 if esh.size != 0:
2812 sh = max(sh, num.sum(esh.max(0)))
2813 if esv.size != 0:
2814 sv = max(sv, num.sum(esv.max(1)))
2815 return sh, sv
2817 def get_grow(self):
2818 ghs, gvs = Widget.get_grow(self)
2819 egh, egv = self.sub_grows_as_array()
2820 if egh.size != 0:
2821 gh = num.sum(egh.max(0))*ghs
2822 else:
2823 gh = 1.0
2824 if egv.size != 0:
2825 gv = num.sum(egv.max(1))*gvs
2826 else:
2827 gv = 1.0
2828 return gh, gv
2830 def set_size(self, size, offset):
2831 (sh, sv), (oh, ov) = self.legalize(size, offset)
2832 esh, esv = self.sub_min_sizes_as_array()
2833 egh, egv = self.sub_grows_as_array()
2835 # available additional space
2836 empty = esh.size == 0
2838 if not empty:
2839 ah = sh - num.sum(esh.max(0))
2840 av = sv - num.sum(esv.max(1))
2841 else:
2842 av = sv
2843 ah = sh
2845 if ah < 0.0:
2846 raise GmtPyError('Container not wide enough for contents '
2847 '(GridLayout, available: %g cm, needed: %g cm)'
2848 % (sh/cm, (num.sum(esh.max(0)))/cm))
2849 if av < 0.0:
2850 raise GmtPyError('Container not high enough for contents '
2851 '(GridLayout, available: %g cm, needed: %g cm)'
2852 % (sv/cm, (num.sum(esv.max(1)))/cm))
2854 nx, ny = esh.shape
2856 if not empty:
2857 # distribute additional space on rows and columns
2858 # according to grow weights and minimal sizes
2859 gsh = egh.sum(1)[:, num.newaxis].repeat(ny, axis=1)
2860 nesh = esh.copy()
2861 nesh += num.where(gsh > 0.0, ah*egh/gsh, 0.0)
2863 nsh = num.maximum(nesh.max(0), esh.max(0))
2865 gsv = egv.sum(0)[num.newaxis, :].repeat(nx, axis=0)
2866 nesv = esv.copy()
2867 nesv += num.where(gsv > 0.0, av*egv/gsv, 0.0)
2868 nsv = num.maximum(nesv.max(1), esv.max(1))
2870 ah = sh - sum(nsh)
2871 av = sv - sum(nsv)
2873 oh += ah/2.
2874 ov += av/2.
2875 sh -= ah
2876 sv -= av
2878 # resize child widgets
2879 neov = ov + sum(nsv)
2880 for row, nesv in zip(self.grid, nsv):
2881 neov -= nesv
2882 neoh = oh
2883 for w, nesh in zip(row, nsh):
2884 w.set_size((nesh, nesv), (neoh, neov))
2885 neoh += nesh
2887 Widget.set_size(self, (sh, sv), (oh, ov))
2889 def set_widget(self, ix, iy, widget=None):
2891 '''
2892 Set one of the sub-widgets.
2894 Sets the sub-widget in column ``ix`` and row ``iy``. The indices are
2895 counted from zero.
2896 '''
2898 if widget is None:
2899 widget = Widget()
2901 self.grid[iy][ix] = widget
2902 widget.set_parent(self)
2904 def get_widget(self, ix, iy):
2906 '''
2907 Get one of the sub-widgets.
2909 Gets the sub-widget from column ``ix`` and row ``iy``. The indices are
2910 counted from zero.
2911 '''
2913 return self.grid[iy][ix]
2915 def get_children(self):
2916 children = []
2917 for row in self.grid:
2918 children.extend(row)
2920 return children
2923def is_gmt5(version='newest'):
2924 return get_gmt_installation(version)['version'][0] in ['5', '6']
2927def is_gmt6(version='newest'):
2928 return get_gmt_installation(version)['version'][0] in ['6']
2931def aspect_for_projection(gmtversion, *args, **kwargs):
2933 gmt = GMT(version=gmtversion, eps_mode=True)
2935 if gmt.is_gmt5():
2936 gmt.psbasemap('-B+gblack', finish=True, *args, **kwargs)
2937 fn = gmt.tempfilename('test.eps')
2938 gmt.save(fn, crop_eps_mode=True)
2939 with open(fn, 'rb') as f:
2940 s = f.read()
2942 l, b, r, t = get_bbox(s) # noqa
2943 else:
2944 gmt.psbasemap('-G0', finish=True, *args, **kwargs)
2945 l, b, r, t = gmt.bbox() # noqa
2947 return (t-b)/(r-l) # noqa
2950def text_box(
2951 text, font=0, font_size=12., angle=0, gmtversion='newest', **kwargs):
2953 gmt = GMT(version=gmtversion)
2954 if gmt.is_gmt5():
2955 row = [0, 0, text]
2956 farg = ['-F+f%gp,%s,%s+j%s' % (font_size, font, 'black', 'BL')]
2957 else:
2958 row = [0, 0, font_size, 0, font, 'BL', text]
2959 farg = []
2961 gmt.pstext(
2962 in_rows=[row],
2963 finish=True,
2964 R=(0, 1, 0, 1),
2965 J='x10p',
2966 N=True,
2967 *farg,
2968 **kwargs)
2970 fn = gmt.tempfilename() + '.ps'
2971 gmt.save(fn)
2973 (_, stderr) = subprocess.Popen(
2974 ['gs', '-q', '-dNOPAUSE', '-dBATCH', '-r720', '-sDEVICE=bbox', fn],
2975 stderr=subprocess.PIPE).communicate()
2977 dx, dy = None, None
2978 for line in stderr.splitlines():
2979 if line.startswith(b'%%HiResBoundingBox:'):
2980 l, b, r, t = [float(x) for x in line.split()[-4:]] # noqa
2981 dx, dy = r-l, t-b # noqa
2982 break
2984 return dx, dy
2987class TableLiner(object):
2988 '''
2989 Utility class to turn tables into lines.
2990 '''
2992 def __init__(self, in_columns=None, in_rows=None, encoding='utf-8'):
2993 self.in_columns = in_columns
2994 self.in_rows = in_rows
2995 self.encoding = encoding
2997 def __iter__(self):
2998 if self.in_columns is not None:
2999 for row in zip(*self.in_columns):
3000 yield (' '.join([str(x) for x in row])+'\n').encode(
3001 self.encoding)
3003 if self.in_rows is not None:
3004 for row in self.in_rows:
3005 yield (' '.join([str(x) for x in row])+'\n').encode(
3006 self.encoding)
3009class LineStreamChopper(object):
3010 '''
3011 File-like object to buffer data.
3012 '''
3014 def __init__(self, liner):
3015 self.chopsize = None
3016 self.liner = liner
3017 self.chop_iterator = None
3018 self.closed = False
3020 def _chopiter(self):
3021 buf = BytesIO()
3022 for line in self.liner:
3023 buf.write(line)
3024 buflen = buf.tell()
3025 if self.chopsize is not None and buflen >= self.chopsize:
3026 buf.seek(0)
3027 while buf.tell() <= buflen-self.chopsize:
3028 yield buf.read(self.chopsize)
3030 newbuf = BytesIO()
3031 newbuf.write(buf.read())
3032 buf.close()
3033 buf = newbuf
3035 yield buf.getvalue()
3036 buf.close()
3038 def read(self, size=None):
3039 if self.closed:
3040 raise ValueError('Cannot read from closed LineStreamChopper.')
3041 if self.chop_iterator is None:
3042 self.chopsize = size
3043 self.chop_iterator = self._chopiter()
3045 self.chopsize = size
3046 try:
3047 return next(self.chop_iterator)
3048 except StopIteration:
3049 return ''
3051 def close(self):
3052 self.chopsize = None
3053 self.chop_iterator = None
3054 self.closed = True
3056 def flush(self):
3057 pass
3060font_tab = {
3061 0: 'Helvetica',
3062 1: 'Helvetica-Bold',
3063}
3065font_tab_rev = dict((v, k) for (k, v) in font_tab.items())
3068class GMT(object):
3069 '''
3070 A thin wrapper to GMT command execution.
3072 A dict ``config`` may be given to override some of the default GMT
3073 parameters. The ``version`` argument may be used to select a specific GMT
3074 version, which should be used with this GMT instance. The selected
3075 version of GMT has to be installed on the system, must be supported by
3076 gmtpy and gmtpy must know where to find it.
3078 Each instance of this class is used for the task of producing one PS or PDF
3079 output file.
3081 Output of a series of GMT commands is accumulated in memory and can then be
3082 saved as PS or PDF file using the :py:meth:`save` method.
3084 GMT commands are accessed as method calls to instances of this class. See
3085 the :py:meth:`__getattr__` method for details on how the method's
3086 arguments are translated into options and arguments for the GMT command.
3088 Associated with each instance of this class, a temporary directory is
3089 created, where temporary files may be created, and which is automatically
3090 deleted, when the object is destroyed. The :py:meth:`tempfilename` method
3091 may be used to get a random filename in the instance's temporary directory.
3093 Any .gmtdefaults files are ignored. The GMT class uses a fixed
3094 set of defaults, which may be altered via an argument to the constructor.
3095 If possible, GMT is run in 'isolation mode', which was introduced with GMT
3096 version 4.2.2, by setting `GMT_TMPDIR` to the instance's temporary
3097 directory. With earlier versions of GMT, problems may arise with parallel
3098 execution of more than one GMT instance.
3100 Each instance of the GMT class may pick a specific version of GMT which
3101 shall be used, so that, if multiple versions of GMT are installed on the
3102 system, different versions of GMT can be used simultaneously such that
3103 backward compatibility of the scripts can be maintained.
3105 '''
3107 def __init__(
3108 self,
3109 config=None,
3110 kontinue=None,
3111 version='newest',
3112 config_papersize=None,
3113 eps_mode=False):
3115 self.installation = get_gmt_installation(version)
3116 self.gmt_config = dict(self.installation['defaults'])
3117 self.eps_mode = eps_mode
3118 self._shutil = shutil
3120 if config:
3121 self.gmt_config.update(config)
3123 if config_papersize:
3124 if not isinstance(config_papersize, str):
3125 config_papersize = 'Custom_%ix%i' % (
3126 int(config_papersize[0]), int(config_papersize[1]))
3128 if self.is_gmt5():
3129 self.gmt_config['PS_MEDIA'] = config_papersize
3130 else:
3131 self.gmt_config['PAPER_MEDIA'] = config_papersize
3133 self.tempdir = tempfile.mkdtemp('', 'gmtpy-')
3134 self.gmt_config_filename = pjoin(self.tempdir, 'gmt.conf')
3135 self.gen_gmt_config_file(self.gmt_config_filename, self.gmt_config)
3137 if kontinue is not None:
3138 self.load_unfinished(kontinue)
3139 self.needstart = False
3140 else:
3141 self.output = BytesIO()
3142 self.needstart = True
3144 self.finished = False
3146 self.environ = os.environ.copy()
3147 self.environ['GMTHOME'] = self.installation.get('home', '')
3148 # GMT isolation mode: works only properly with GMT version >= 4.2.2
3149 self.environ['GMT_TMPDIR'] = self.tempdir
3151 self.layout = None
3152 self.command_log = []
3153 self.keep_temp_dir = False
3155 def is_gmt5(self):
3156 return self.get_version()[0] in ['5', '6']
3158 def is_gmt6(self):
3159 return self.get_version()[0] in ['6']
3161 def get_version(self):
3162 return self.installation['version']
3164 def get_config(self, key):
3165 return self.gmt_config[key]
3167 def to_points(self, string):
3168 if not string:
3169 return 0
3171 unit = string[-1]
3172 if unit in _units:
3173 return float(string[:-1])/_units[unit]
3174 else:
3175 default_unit = measure_unit(self.gmt_config).lower()[0]
3176 return float(string)/_units[default_unit]
3178 def label_font_size(self):
3179 if self.is_gmt5():
3180 return self.to_points(self.gmt_config['FONT_LABEL'].split(',')[0])
3181 else:
3182 return self.to_points(self.gmt_config['LABEL_FONT_SIZE'])
3184 def label_font(self):
3185 if self.is_gmt5():
3186 return font_tab_rev(self.gmt_config['FONT_LABEL'].split(',')[1])
3187 else:
3188 return self.gmt_config['LABEL_FONT']
3190 def gen_gmt_config_file(self, config_filename, config):
3191 f = open(config_filename, 'wb')
3192 f.write(
3193 ('#\n# GMT %s Defaults file\n'
3194 % self.installation['version']).encode('ascii'))
3196 for k, v in config.items():
3197 f.write(('%s = %s\n' % (k, v)).encode('ascii'))
3198 f.close()
3200 def __del__(self):
3201 if not self.keep_temp_dir:
3202 self._shutil.rmtree(self.tempdir)
3204 def _gmtcommand(self, command, *addargs, **kwargs):
3206 '''
3207 Execute arbitrary GMT command.
3209 See docstring in __getattr__ for details.
3210 '''
3212 in_stream = kwargs.pop('in_stream', None)
3213 in_filename = kwargs.pop('in_filename', None)
3214 in_string = kwargs.pop('in_string', None)
3215 in_columns = kwargs.pop('in_columns', None)
3216 in_rows = kwargs.pop('in_rows', None)
3217 out_stream = kwargs.pop('out_stream', None)
3218 out_filename = kwargs.pop('out_filename', None)
3219 out_discard = kwargs.pop('out_discard', None)
3220 finish = kwargs.pop('finish', False)
3221 suppressdefaults = kwargs.pop('suppress_defaults', False)
3222 config_override = kwargs.pop('config', None)
3224 assert not self.finished
3226 # check for mutual exclusiveness on input and output possibilities
3227 assert (1 >= len(
3228 [x for x in [
3229 in_stream, in_filename, in_string, in_columns, in_rows]
3230 if x is not None]))
3231 assert (1 >= len([x for x in [out_stream, out_filename, out_discard]
3232 if x is not None]))
3234 options = []
3236 gmt_config = self.gmt_config
3237 if not self.is_gmt5():
3238 gmt_config_filename = self.gmt_config_filename
3239 if config_override:
3240 gmt_config = self.gmt_config.copy()
3241 gmt_config.update(config_override)
3242 gmt_config_override_filename = pjoin(
3243 self.tempdir, 'gmtdefaults_override')
3244 self.gen_gmt_config_file(
3245 gmt_config_override_filename, gmt_config)
3246 gmt_config_filename = gmt_config_override_filename
3248 else: # gmt5 needs override variables as --VAR=value
3249 if config_override:
3250 for k, v in config_override.items():
3251 options.append('--%s=%s' % (k, v))
3253 if out_discard:
3254 out_filename = '/dev/null'
3256 out_mustclose = False
3257 if out_filename is not None:
3258 out_mustclose = True
3259 out_stream = open(out_filename, 'wb')
3261 if in_filename is not None:
3262 in_stream = open(in_filename, 'rb')
3264 if in_string is not None:
3265 in_stream = BytesIO(in_string)
3267 encoding_gmt = gmt_config.get(
3268 'PS_CHAR_ENCODING',
3269 gmt_config.get('CHAR_ENCODING', 'ISOLatin1+'))
3271 encoding = encoding_gmt_to_python[encoding_gmt.lower()]
3273 if in_columns is not None or in_rows is not None:
3274 in_stream = LineStreamChopper(TableLiner(in_columns=in_columns,
3275 in_rows=in_rows,
3276 encoding=encoding))
3278 # convert option arguments to strings
3279 for k, v in kwargs.items():
3280 if len(k) > 1:
3281 raise GmtPyError('Found illegal keyword argument "%s" '
3282 'while preparing options for command "%s"'
3283 % (k, command))
3285 if type(v) is bool:
3286 if v:
3287 options.append('-%s' % k)
3288 elif type(v) is tuple or type(v) is list:
3289 options.append('-%s' % k + '/'.join([str(x) for x in v]))
3290 else:
3291 options.append('-%s%s' % (k, str(v)))
3293 # if not redirecting to an external sink, handle -K -O
3294 if out_stream is None:
3295 if not finish:
3296 options.append('-K')
3297 else:
3298 self.finished = True
3300 if not self.needstart:
3301 options.append('-O')
3302 else:
3303 self.needstart = False
3305 out_stream = self.output
3307 # run the command
3308 if self.is_gmt5():
3309 args = [pjoin(self.installation['bin'], 'gmt'), command]
3310 else:
3311 args = [pjoin(self.installation['bin'], command)]
3313 if not os.path.isfile(args[0]):
3314 raise OSError('No such file: %s' % args[0])
3315 args.extend(options)
3316 args.extend(addargs)
3317 if not self.is_gmt5() and not suppressdefaults:
3318 # does not seem to work with GMT 5 (and should not be necessary
3319 args.append('+'+gmt_config_filename)
3321 bs = 2048
3322 p = subprocess.Popen(args, stdin=subprocess.PIPE,
3323 stdout=subprocess.PIPE, bufsize=bs,
3324 env=self.environ)
3325 while True:
3326 cr, cw, cx = select([p.stdout], [p.stdin], [])
3327 if cr:
3328 out_stream.write(p.stdout.read(bs))
3329 if cw:
3330 if in_stream is not None:
3331 data = in_stream.read(bs)
3332 if len(data) == 0:
3333 break
3334 p.stdin.write(data)
3335 else:
3336 break
3337 if not cr and not cw:
3338 break
3340 p.stdin.close()
3342 while True:
3343 data = p.stdout.read(bs)
3344 if len(data) == 0:
3345 break
3346 out_stream.write(data)
3348 p.stdout.close()
3350 retcode = p.wait()
3352 if in_stream is not None:
3353 in_stream.close()
3355 if out_mustclose:
3356 out_stream.close()
3358 if retcode != 0:
3359 self.keep_temp_dir = True
3360 raise GMTError('Command %s returned an error. '
3361 'While executing command:\n%s'
3362 % (command, escape_shell_args(args)))
3364 self.command_log.append(args)
3366 def __getattr__(self, command):
3368 '''
3369 Maps to call self._gmtcommand(command, \\*addargs, \\*\\*kwargs).
3371 Execute arbitrary GMT command.
3373 Run a GMT command and by default append its postscript output to the
3374 output file maintained by the GMT instance on which this method is
3375 called.
3377 Except for a few keyword arguments listed below, any ``kwargs`` and
3378 ``addargs`` are converted into command line options and arguments and
3379 passed to the GMT command. Numbers in keyword arguments are converted
3380 into strings. E.g. ``S=10`` is translated into ``'-S10'``. Tuples of
3381 numbers or strings are converted into strings where the elements of the
3382 tuples are separated by slashes '/'. E.g. ``R=(10, 10, 20, 20)`` is
3383 translated into ``'-R10/10/20/20'``. Options with a boolean argument
3384 are only appended to the GMT command, if their values are True.
3386 If no output redirection is in effect, the -K and -O options are
3387 handled by gmtpy and thus should not be specified. Use
3388 ``out_discard=True`` if you don't want -K or -O beeing added, but are
3389 not interested in the output.
3391 The standard input of the GMT process is fed by data selected with one
3392 of the following ``in_*`` keyword arguments:
3394 =============== =======================================================
3395 ``in_stream`` Data is read from an open file like object.
3396 ``in_filename`` Data is read from the given file.
3397 ``in_string`` String content is dumped to the process.
3398 ``in_columns`` A 2D nested iterable whose elements can be accessed as
3399 ``in_columns[icolumn][irow]`` is converted into an
3400 ascii
3401 table, which is fed to the process.
3402 ``in_rows`` A 2D nested iterable whos elements can be accessed as
3403 ``in_rows[irow][icolumn]`` is converted into an ascii
3404 table, which is fed to the process.
3405 =============== =======================================================
3407 The standard output of the GMT process may be redirected by one of the
3408 following options:
3410 ================= =====================================================
3411 ``out_stream`` Output is fed to an open file like object.
3412 ``out_filename`` Output is dumped to the given file.
3413 ``out_discard`` If True, output is dumped to :file:`/dev/null`.
3414 ================= =====================================================
3416 Additional keyword arguments:
3418 ===================== =================================================
3419 ``config`` Dict with GMT defaults which override the
3420 currently active set of defaults exclusively
3421 during this call.
3422 ``finish`` If True, the postscript file, which is maintained
3423 by the GMT instance is finished, and no further
3424 plotting is allowed.
3425 ``suppress_defaults`` Suppress appending of the ``'+gmtdefaults'``
3426 option to the command.
3427 ===================== =================================================
3429 '''
3431 def f(*args, **kwargs):
3432 return self._gmtcommand(command, *args, **kwargs)
3433 return f
3435 def tempfilename(self, name=None):
3436 '''
3437 Get filename for temporary file in the private temp directory.
3439 If no ``name`` argument is given, a random name is picked. If
3440 ``name`` is given, returns a path ending in that ``name``.
3441 '''
3443 if not name:
3444 name = ''.join(
3445 [random.choice('abcdefghijklmnopqrstuvwxyz')
3446 for i in range(10)])
3448 fn = pjoin(self.tempdir, name)
3449 return fn
3451 def tempfile(self, name=None):
3452 '''
3453 Create and open a file in the private temp directory.
3454 '''
3456 fn = self.tempfilename(name)
3457 f = open(fn, 'wb')
3458 return f, fn
3460 def save_unfinished(self, filename):
3461 out = open(filename, 'wb')
3462 out.write(self.output.getvalue())
3463 out.close()
3465 def load_unfinished(self, filename):
3466 self.output = BytesIO()
3467 self.finished = False
3468 inp = open(filename, 'rb')
3469 self.output.write(inp.read())
3470 inp.close()
3472 def dump(self, ident):
3473 filename = self.tempfilename('breakpoint-%s' % ident)
3474 self.save_unfinished(filename)
3476 def load(self, ident):
3477 filename = self.tempfilename('breakpoint-%s' % ident)
3478 self.load_unfinished(filename)
3480 def save(self, filename=None, bbox=None, resolution=150, oversample=2.,
3481 width=None, height=None, size=None, crop_eps_mode=False,
3482 psconvert=False):
3484 '''
3485 Finish and save figure as PDF, PS or PPM file.
3487 If filename ends with ``'.pdf'`` a PDF file is created by piping the
3488 GMT output through :program:`gmtpy-epstopdf`.
3490 If filename ends with ``'.png'`` a PNG file is created by running
3491 :program:`gmtpy-epstopdf`, :program:`pdftocairo` and
3492 :program:`convert`. ``resolution`` specifies the resolution in DPI for
3493 raster file formats. Rasterization is done at a higher resolution if
3494 ``oversample`` is set to a value higher than one. The output image size
3495 can also be controlled by setting ``width``, ``height`` or ``size``
3496 instead of ``resolution``. When ``size`` is given, the image is scaled
3497 so that ``max(width, height) == size``.
3499 The bounding box is set according to the values given in ``bbox``.
3500 '''
3502 if not self.finished:
3503 self.psxy(R=True, J=True, finish=True)
3505 if filename:
3506 tempfn = pjoin(self.tempdir, 'incomplete')
3507 out = open(tempfn, 'wb')
3508 else:
3509 out = sys.stdout
3511 if bbox and not self.is_gmt5():
3512 out.write(replace_bbox(bbox, self.output.getvalue()))
3513 else:
3514 out.write(self.output.getvalue())
3516 if filename:
3517 out.close()
3519 if filename.endswith('.ps') or (
3520 not self.is_gmt5() and filename.endswith('.eps')):
3522 shutil.move(tempfn, filename)
3523 return
3525 if self.is_gmt5():
3526 if crop_eps_mode:
3527 addarg = ['-A']
3528 else:
3529 addarg = []
3531 subprocess.call(
3532 [pjoin(self.installation['bin'], 'gmt'), 'psconvert',
3533 '-Te', '-F%s' % tempfn, tempfn, ] + addarg)
3535 if bbox:
3536 with open(tempfn + '.eps', 'rb') as fin:
3537 with open(tempfn + '-fixbb.eps', 'wb') as fout:
3538 replace_bbox(bbox, fin, fout)
3540 shutil.move(tempfn + '-fixbb.eps', tempfn + '.eps')
3542 else:
3543 shutil.move(tempfn, tempfn + '.eps')
3545 if filename.endswith('.eps'):
3546 shutil.move(tempfn + '.eps', filename)
3547 return
3549 elif filename.endswith('.pdf'):
3550 if psconvert:
3551 gmt_bin = pjoin(self.installation['bin'], 'gmt')
3552 subprocess.call([gmt_bin, 'psconvert', tempfn + '.eps', '-Tf',
3553 '-F' + filename])
3554 else:
3555 subprocess.call(['gmtpy-epstopdf', '--res=%i' % resolution,
3556 '--outfile=' + filename, tempfn + '.eps'])
3557 else:
3558 subprocess.call([
3559 'gmtpy-epstopdf',
3560 '--res=%i' % (resolution * oversample),
3561 '--outfile=' + tempfn + '.pdf', tempfn + '.eps'])
3563 convert_graph(
3564 tempfn + '.pdf', filename,
3565 resolution=resolution, oversample=oversample,
3566 size=size, width=width, height=height)
3568 def bbox(self):
3569 return get_bbox(self.output.getvalue())
3571 def get_command_log(self):
3572 '''
3573 Get the command log.
3574 '''
3576 return self.command_log
3578 def __str__(self):
3579 s = ''
3580 for com in self.command_log:
3581 s += com[0] + '\n ' + '\n '.join(com[1:]) + '\n\n'
3582 return s
3584 def page_size_points(self):
3585 '''
3586 Try to get paper size of output postscript file in points.
3587 '''
3589 pm = paper_media(self.gmt_config).lower()
3590 if pm.endswith('+') or pm.endswith('-'):
3591 pm = pm[:-1]
3593 orient = page_orientation(self.gmt_config).lower()
3595 if pm in all_paper_sizes():
3597 if orient == 'portrait':
3598 return get_paper_size(pm)
3599 else:
3600 return get_paper_size(pm)[1], get_paper_size(pm)[0]
3602 m = re.match(r'custom_([0-9.]+)([cimp]?)x([0-9.]+)([cimp]?)', pm)
3603 if m:
3604 w, uw, h, uh = m.groups()
3605 w, h = float(w), float(h)
3606 if uw:
3607 w *= _units[uw]
3608 if uh:
3609 h *= _units[uh]
3610 if orient == 'portrait':
3611 return w, h
3612 else:
3613 return h, w
3615 return None, None
3617 def default_layout(self, with_palette=False):
3618 '''
3619 Get a default layout for the output page.
3621 One of three different layouts is choosen, depending on the
3622 `PAPER_MEDIA` setting in the GMT configuration dict.
3624 If `PAPER_MEDIA` ends with a ``'+'`` (EPS output is selected), a
3625 :py:class:`FrameLayout` is centered on the page, whose size is
3626 controlled by its center widget's size plus the margins of the
3627 :py:class:`FrameLayout`.
3629 If `PAPER_MEDIA` indicates, that a custom page size is wanted by
3630 starting with ``'Custom_'``, a :py:class:`FrameLayout` is used to fill
3631 the complete page. The center widget's size is then controlled by the
3632 page's size minus the margins of the :py:class:`FrameLayout`.
3634 In any other case, two FrameLayouts are nested, such that the outer
3635 layout attaches a 1 cm (printer) margin around the complete page, and
3636 the inner FrameLayout's center widget takes up as much space as
3637 possible under the constraint, that an aspect ratio of 1/golden_ratio
3638 is preserved.
3640 In any case, a reference to the innermost :py:class:`FrameLayout`
3641 instance is returned. The top-level layout can be accessed by calling
3642 :py:meth:`Widget.get_parent` on the returned layout.
3643 '''
3645 if self.layout is None:
3646 w, h = self.page_size_points()
3648 if w is None or h is None:
3649 raise GmtPyError("Can't determine page size for layout")
3651 pm = paper_media(self.gmt_config).lower()
3653 if with_palette:
3654 palette_layout = GridLayout(3, 1)
3655 spacer = palette_layout.get_widget(1, 0)
3656 palette_widget = palette_layout.get_widget(2, 0)
3657 spacer.set_horizontal(0.5*cm)
3658 palette_widget.set_horizontal(0.5*cm)
3660 if pm.endswith('+') or self.eps_mode:
3661 outer = CenterLayout()
3662 outer.set_policy((w, h), (0., 0.))
3663 inner = FrameLayout()
3664 outer.set_widget(inner)
3665 if with_palette:
3666 inner.set_widget('center', palette_layout)
3667 widget = palette_layout
3668 else:
3669 widget = inner.get_widget('center')
3670 widget.set_policy((w/golden_ratio, 0.), (0., 0.),
3671 aspect=1./golden_ratio)
3672 mw = 3.0*cm
3673 inner.set_fixed_margins(
3674 mw, mw, mw/golden_ratio, mw/golden_ratio)
3675 self.layout = inner
3677 elif pm.startswith('custom_'):
3678 layout = FrameLayout()
3679 layout.set_policy((w, h), (0., 0.))
3680 mw = 3.0*cm
3681 layout.set_min_margins(
3682 mw, mw, mw/golden_ratio, mw/golden_ratio)
3683 if with_palette:
3684 layout.set_widget('center', palette_layout)
3685 self.layout = layout
3686 else:
3687 outer = FrameLayout()
3688 outer.set_policy((w, h), (0., 0.))
3689 outer.set_fixed_margins(1.*cm, 1.*cm, 1.*cm, 1.*cm)
3691 inner = FrameLayout()
3692 outer.set_widget('center', inner)
3693 mw = 3.0*cm
3694 inner.set_min_margins(mw, mw, mw/golden_ratio, mw/golden_ratio)
3695 if with_palette:
3696 inner.set_widget('center', palette_layout)
3697 widget = palette_layout
3698 else:
3699 widget = inner.get_widget('center')
3701 widget.set_aspect(1./golden_ratio)
3703 self.layout = inner
3705 return self.layout
3707 def draw_layout(self, layout):
3708 '''
3709 Use psxy to draw layout; for debugging
3710 '''
3712 # corners = layout.get_corners(descend=True)
3713 rects = num.array(layout.get_sizes(), dtype=float)
3714 rects_wid = rects[:, 0, 0]
3715 rects_hei = rects[:, 0, 1]
3716 rects_center_x = rects[:, 1, 0] + rects_wid*0.5
3717 rects_center_y = rects[:, 1, 1] + rects_hei*0.5
3718 nrects = len(rects)
3719 prects = (rects_center_x, rects_center_y, num.arange(nrects),
3720 num.zeros(nrects), rects_hei, rects_wid)
3722 # points = num.array(corners, dtype=float)
3724 cptfile = self.tempfilename() + '.cpt'
3725 self.makecpt(
3726 C='ocean',
3727 T='%g/%g/%g' % (-nrects, nrects, 1),
3728 Z=True,
3729 out_filename=cptfile, suppress_defaults=True)
3731 bb = layout.bbox()
3732 self.psxy(
3733 in_columns=prects,
3734 C=cptfile,
3735 W='1p',
3736 S='J',
3737 R=(bb[0], bb[2], bb[1], bb[3]),
3738 *layout.XYJ())
3741def simpleconf_to_ax(conf, axname):
3742 c = {}
3743 x = axname
3744 for x in ('', axname):
3745 for k in ('label', 'unit', 'scaled_unit', 'scaled_unit_factor',
3746 'space', 'mode', 'approx_ticks', 'limits', 'masking', 'inc',
3747 'snap'):
3749 if x+k in conf:
3750 c[k] = conf[x+k]
3752 return Ax(**c)
3755class DensityPlotDef(object):
3756 def __init__(self, data, cpt='ocean', tension=0.7, size=(640, 480),
3757 contour=False, method='surface', zscaler=None, **extra):
3758 self.data = data
3759 self.cpt = cpt
3760 self.tension = tension
3761 self.size = size
3762 self.contour = contour
3763 self.method = method
3764 self.zscaler = zscaler
3765 self.extra = extra
3768class TextDef(object):
3769 def __init__(
3770 self,
3771 data,
3772 size=9,
3773 justify='MC',
3774 fontno=0,
3775 offset=(0, 0),
3776 color='black'):
3778 self.data = data
3779 self.size = size
3780 self.justify = justify
3781 self.fontno = fontno
3782 self.offset = offset
3783 self.color = color
3786class Simple(object):
3787 def __init__(self, gmtconfig=None, gmtversion='newest', **simple_config):
3788 self.data = []
3789 self.symbols = []
3790 self.config = copy.deepcopy(simple_config)
3791 self.gmtconfig = gmtconfig
3792 self.density_plot_defs = []
3793 self.text_defs = []
3795 self.gmtversion = gmtversion
3797 self.data_x = []
3798 self.symbols_x = []
3800 self.data_y = []
3801 self.symbols_y = []
3803 self.default_config = {}
3804 self.set_defaults(width=15.*cm,
3805 height=15.*cm / golden_ratio,
3806 margins=(2.*cm, 2.*cm, 2.*cm, 2.*cm),
3807 with_palette=False,
3808 palette_offset=0.5*cm,
3809 palette_width=None,
3810 palette_height=None,
3811 zlabeloffset=2*cm,
3812 draw_layout=False)
3814 self.setup_defaults()
3815 self.fixate_widget_aspect = False
3817 def setup_defaults(self):
3818 pass
3820 def set_defaults(self, **kwargs):
3821 self.default_config.update(kwargs)
3823 def plot(self, data, symbol=''):
3824 self.data.append(data)
3825 self.symbols.append(symbol)
3827 def density_plot(self, data, **kwargs):
3828 dpd = DensityPlotDef(data, **kwargs)
3829 self.density_plot_defs.append(dpd)
3831 def text(self, data, **kwargs):
3832 dpd = TextDef(data, **kwargs)
3833 self.text_defs.append(dpd)
3835 def plot_x(self, data, symbol=''):
3836 self.data_x.append(data)
3837 self.symbols_x.append(symbol)
3839 def plot_y(self, data, symbol=''):
3840 self.data_y.append(data)
3841 self.symbols_y.append(symbol)
3843 def set(self, **kwargs):
3844 self.config.update(kwargs)
3846 def setup_base(self, conf):
3847 w = conf.pop('width')
3848 h = conf.pop('height')
3849 margins = conf.pop('margins')
3851 gmtconfig = {}
3852 if self.gmtconfig is not None:
3853 gmtconfig.update(self.gmtconfig)
3855 gmt = GMT(
3856 version=self.gmtversion,
3857 config=gmtconfig,
3858 config_papersize='Custom_%ix%i' % (w, h))
3860 layout = gmt.default_layout(with_palette=conf['with_palette'])
3861 layout.set_min_margins(*margins)
3862 if conf['with_palette']:
3863 widget = layout.get_widget().get_widget(0, 0)
3864 spacer = layout.get_widget().get_widget(1, 0)
3865 spacer.set_horizontal(conf['palette_offset'])
3866 palette_widget = layout.get_widget().get_widget(2, 0)
3867 if conf['palette_width'] is not None:
3868 palette_widget.set_horizontal(conf['palette_width'])
3869 if conf['palette_height'] is not None:
3870 palette_widget.set_vertical(conf['palette_height'])
3871 widget.set_vertical(h-margins[2]-margins[3]-0.03*cm)
3872 return gmt, layout, widget, palette_widget
3873 else:
3874 widget = layout.get_widget()
3875 return gmt, layout, widget, None
3877 def setup_projection(self, widget, scaler, conf):
3878 pass
3880 def setup_scaling(self, conf):
3881 ndims = 2
3882 if self.density_plot_defs:
3883 ndims = 3
3885 axes = [simpleconf_to_ax(conf, x) for x in 'xyz'[:ndims]]
3887 data_all = []
3888 data_all.extend(self.data)
3889 for dsd in self.density_plot_defs:
3890 if dsd.zscaler is None:
3891 data_all.append(dsd.data)
3892 else:
3893 data_all.append(dsd.data[:2])
3894 data_chopped = [ds[:ndims] for ds in data_all]
3896 scaler = ScaleGuru(data_chopped, axes=axes[:ndims])
3898 self.setup_scaling_plus(scaler, axes[:ndims])
3900 return scaler
3902 def setup_scaling_plus(self, scaler, axes):
3903 pass
3905 def setup_scaling_extra(self, scaler, conf):
3907 scaler_x = scaler.copy()
3908 scaler_x.data_ranges[1] = (0., 1.)
3909 scaler_x.axes[1].mode = 'off'
3911 scaler_y = scaler.copy()
3912 scaler_y.data_ranges[0] = (0., 1.)
3913 scaler_y.axes[0].mode = 'off'
3915 return scaler_x, scaler_y
3917 def draw_density(self, gmt, widget, scaler):
3919 R = scaler.R()
3920 # par = scaler.get_params()
3921 rxyj = R + widget.XYJ()
3922 innerticks = False
3923 for dpd in self.density_plot_defs:
3925 fn_cpt = gmt.tempfilename() + '.cpt'
3927 if dpd.zscaler is not None:
3928 s = dpd.zscaler
3929 else:
3930 s = scaler
3932 gmt.makecpt(C=dpd.cpt, out_filename=fn_cpt, *s.T())
3934 fn_grid = gmt.tempfilename()
3936 fn_mean = gmt.tempfilename()
3938 if dpd.method in ('surface', 'triangulate'):
3939 gmt.blockmean(in_columns=dpd.data,
3940 I='%i+/%i+' % dpd.size, # noqa
3941 out_filename=fn_mean, *R)
3943 if dpd.method == 'surface':
3944 gmt.surface(
3945 in_filename=fn_mean,
3946 T=dpd.tension,
3947 G=fn_grid,
3948 I='%i+/%i+' % dpd.size, # noqa
3949 out_discard=True,
3950 *R)
3952 if dpd.method == 'triangulate':
3953 gmt.triangulate(
3954 in_filename=fn_mean,
3955 G=fn_grid,
3956 I='%i+/%i+' % dpd.size, # noqa
3957 out_discard=True,
3958 V=True,
3959 *R)
3961 if gmt.is_gmt5():
3962 gmt.grdimage(fn_grid, C=fn_cpt, E='i', n='l', *rxyj)
3964 else:
3965 gmt.grdimage(fn_grid, C=fn_cpt, E='i', S='l', *rxyj)
3967 if dpd.contour:
3968 gmt.grdcontour(fn_grid, C=fn_cpt, W='0.5p,black', *rxyj)
3969 innerticks = '0.5p,black'
3971 os.remove(fn_grid)
3972 os.remove(fn_mean)
3974 if dpd.method == 'fillcontour':
3975 extra = dict(C=fn_cpt)
3976 extra.update(dpd.extra)
3977 gmt.pscontour(in_columns=dpd.data,
3978 I=True, *rxyj, **extra) # noqa
3980 if dpd.method == 'contour':
3981 extra = dict(W='0.5p,black', C=fn_cpt)
3982 extra.update(dpd.extra)
3983 gmt.pscontour(in_columns=dpd.data, *rxyj, **extra)
3985 return fn_cpt, innerticks
3987 def draw_basemap(self, gmt, widget, scaler):
3988 gmt.psbasemap(*(widget.JXY() + scaler.RB(ax_projection=True)))
3990 def draw(self, gmt, widget, scaler):
3991 rxyj = scaler.R() + widget.JXY()
3992 for dat, sym in zip(self.data, self.symbols):
3993 gmt.psxy(in_columns=dat, *(sym.split()+rxyj))
3995 def post_draw(self, gmt, widget, scaler):
3996 pass
3998 def pre_draw(self, gmt, widget, scaler):
3999 pass
4001 def draw_extra(self, gmt, widget, scaler_x, scaler_y):
4003 for dat, sym in zip(self.data_x, self.symbols_x):
4004 gmt.psxy(in_columns=dat,
4005 *(sym.split() + scaler_x.R() + widget.JXY()))
4007 for dat, sym in zip(self.data_y, self.symbols_y):
4008 gmt.psxy(in_columns=dat,
4009 *(sym.split() + scaler_y.R() + widget.JXY()))
4011 def draw_text(self, gmt, widget, scaler):
4013 rxyj = scaler.R() + widget.JXY()
4014 for td in self.text_defs:
4015 x, y = td.data[0:2]
4016 text = td.data[-1]
4017 size = td.size
4018 angle = 0
4019 fontno = td.fontno
4020 justify = td.justify
4021 color = td.color
4022 if gmt.is_gmt5():
4023 gmt.pstext(
4024 in_rows=[(x, y, text)],
4025 F='+f%gp,%s,%s+a%g+j%s' % (
4026 size, fontno, color, angle, justify),
4027 D='%gp/%gp' % td.offset, *rxyj)
4028 else:
4029 gmt.pstext(
4030 in_rows=[(x, y, size, angle, fontno, justify, text)],
4031 D='%gp/%gp' % td.offset, *rxyj)
4033 def save(self, filename, resolution=150):
4035 conf = dict(self.default_config)
4036 conf.update(self.config)
4038 gmt, layout, widget, palette_widget = self.setup_base(conf)
4039 scaler = self.setup_scaling(conf)
4040 scaler_x, scaler_y = self.setup_scaling_extra(scaler, conf)
4042 self.setup_projection(widget, scaler, conf)
4043 if self.fixate_widget_aspect:
4044 aspect = aspect_for_projection(
4045 gmt.installation['version'], *(widget.J() + scaler.R()))
4047 widget.set_aspect(aspect)
4049 if conf['draw_layout']:
4050 gmt.draw_layout(layout)
4051 cptfile = None
4052 if self.density_plot_defs:
4053 cptfile, innerticks = self.draw_density(gmt, widget, scaler)
4054 self.pre_draw(gmt, widget, scaler)
4055 self.draw(gmt, widget, scaler)
4056 self.post_draw(gmt, widget, scaler)
4057 self.draw_extra(gmt, widget, scaler_x, scaler_y)
4058 self.draw_text(gmt, widget, scaler)
4059 self.draw_basemap(gmt, widget, scaler)
4061 if palette_widget and cptfile:
4062 nice_palette(gmt, palette_widget, scaler, cptfile,
4063 innerticks=innerticks,
4064 zlabeloffset=conf['zlabeloffset'])
4066 gmt.save(filename, resolution=resolution)
4069class LinLinPlot(Simple):
4070 pass
4073class LogLinPlot(Simple):
4075 def setup_defaults(self):
4076 self.set_defaults(xmode='min-max')
4078 def setup_projection(self, widget, scaler, conf):
4079 widget['J'] = '-JX%(width)gpl/%(height)gp'
4080 scaler['B'] = '-B2:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen'
4083class LinLogPlot(Simple):
4085 def setup_defaults(self):
4086 self.set_defaults(ymode='min-max')
4088 def setup_projection(self, widget, scaler, conf):
4089 widget['J'] = '-JX%(width)gp/%(height)gpl'
4090 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/2:%(ylabel)s:WSen'
4093class LogLogPlot(Simple):
4095 def setup_defaults(self):
4096 self.set_defaults(mode='min-max')
4098 def setup_projection(self, widget, scaler, conf):
4099 widget['J'] = '-JX%(width)gpl/%(height)gpl'
4100 scaler['B'] = '-B2:%(xlabel)s:/2:%(ylabel)s:WSen'
4103class AziDistPlot(Simple):
4105 def __init__(self, *args, **kwargs):
4106 Simple.__init__(self, *args, **kwargs)
4107 self.fixate_widget_aspect = True
4109 def setup_defaults(self):
4110 self.set_defaults(
4111 height=15.*cm,
4112 width=15.*cm,
4113 xmode='off',
4114 xlimits=(0., 360.),
4115 xinc=45.)
4117 def setup_projection(self, widget, scaler, conf):
4118 widget['J'] = '-JPa%(width)gp'
4120 def setup_scaling_plus(self, scaler, axes):
4121 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/%(yinc)g:%(ylabel)s:N'
4124class MPlot(Simple):
4126 def __init__(self, *args, **kwargs):
4127 Simple.__init__(self, *args, **kwargs)
4128 self.fixate_widget_aspect = True
4130 def setup_defaults(self):
4131 self.set_defaults(xmode='min-max', ymode='min-max')
4133 def setup_projection(self, widget, scaler, conf):
4134 par = scaler.get_params()
4135 lon0 = (par['xmin'] + par['xmax'])/2.
4136 lat0 = (par['ymin'] + par['ymax'])/2.
4137 sll = '%g/%g' % (lon0, lat0)
4138 widget['J'] = '-JM' + sll + '/%(width)gp'
4139 scaler['B'] = \
4140 '-B%(xinc)gg%(xinc)g:%(xlabel)s:/%(yinc)gg%(yinc)g:%(ylabel)s:WSen'
4143def nice_palette(gmt, widget, scaleguru, cptfile, zlabeloffset=0.8*inch,
4144 innerticks=True):
4146 par = scaleguru.get_params()
4147 par_ax = scaleguru.get_params(ax_projection=True)
4148 nz_palette = int(widget.height()/inch * 300)
4149 px = num.zeros(nz_palette*2)
4150 px[1::2] += 1
4151 pz = num.linspace(par['zmin'], par['zmax'], nz_palette).repeat(2)
4152 pdz = pz[2]-pz[0]
4153 palgrdfile = gmt.tempfilename()
4154 pal_r = (0, 1, par['zmin'], par['zmax'])
4155 pal_ax_r = (0, 1, par_ax['zmin'], par_ax['zmax'])
4156 gmt.xyz2grd(
4157 G=palgrdfile, R=pal_r,
4158 I=(1, pdz), in_columns=(px, pz, pz), # noqa
4159 out_discard=True)
4161 gmt.grdimage(palgrdfile, R=pal_r, C=cptfile, *widget.JXY())
4162 if isinstance(innerticks, str):
4163 tickpen = innerticks
4164 gmt.grdcontour(palgrdfile, W=tickpen, R=pal_r, C=cptfile,
4165 *widget.JXY())
4167 negpalwid = '%gp' % -widget.width()
4168 if not isinstance(innerticks, str) and innerticks:
4169 ticklen = negpalwid
4170 else:
4171 ticklen = '0p'
4173 TICK_LENGTH_PARAM = 'MAP_TICK_LENGTH' if gmt.is_gmt5() else 'TICK_LENGTH'
4174 gmt.psbasemap(
4175 R=pal_ax_r, B='4::/%(zinc)g::nsw' % par_ax,
4176 config={TICK_LENGTH_PARAM: ticklen},
4177 *widget.JXY())
4179 if innerticks:
4180 gmt.psbasemap(
4181 R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax,
4182 config={TICK_LENGTH_PARAM: '0p'},
4183 *widget.JXY())
4184 else:
4185 gmt.psbasemap(R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax, *widget.JXY())
4187 if par_ax['zlabel']:
4188 label_font = gmt.label_font()
4189 label_font_size = gmt.label_font_size()
4190 label_offset = zlabeloffset
4191 gmt.pstext(
4192 R=(0, 1, 0, 2), D='%gp/0p' % label_offset,
4193 N=True,
4194 in_rows=[(1, 1, label_font_size, -90, label_font, 'CB',
4195 par_ax['zlabel'])],
4196 *widget.JXY())