Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/plot/gmtpy.py: 77%
1635 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-06 06:59 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-06 06:59 +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 *= var.scale_factor
1524 if hasattr(var, 'add_offset'):
1525 arr += var.add_offset
1527 return arr
1530def loadgrd(filename):
1531 '''
1532 Read COARDS compliant netcdf (grd) file.
1533 '''
1535 nc = netcdf_file(filename, 'r')
1536 vkeys = list(nc.variables.keys())
1537 kx = 'x'
1538 ky = 'y'
1539 if 'lon' in vkeys:
1540 kx = 'lon'
1541 if 'lat' in vkeys:
1542 ky = 'lat'
1544 kz = 'z'
1545 if 'altitude' in vkeys:
1546 kz = 'altitude'
1548 x = to_array(nc.variables[kx])
1549 y = to_array(nc.variables[ky])
1550 z = to_array(nc.variables[kz])
1552 nc.close()
1553 return x, y, z
1556def centers_to_edges(asorted):
1557 return (asorted[1:] + asorted[:-1])/2.
1560def nvals(asorted):
1561 eps = (asorted[-1]-asorted[0])/asorted.size
1562 return num.sum(asorted[1:] - asorted[:-1] >= eps) + 1
1565def guess_vals(asorted):
1566 eps = (asorted[-1]-asorted[0])/asorted.size
1567 indis = num.nonzero(asorted[1:] - asorted[:-1] >= eps)[0]
1568 indis = num.concatenate((num.array([0]), indis+1,
1569 num.array([asorted.size])))
1570 asum = num.zeros(asorted.size+1)
1571 asum[1:] = num.cumsum(asorted)
1572 return (asum[indis[1:]] - asum[indis[:-1]]) / (indis[1:]-indis[:-1])
1575def blockmean(asorted, b):
1576 indis = num.nonzero(asorted[1:] - asorted[:-1])[0]
1577 indis = num.concatenate((num.array([0]), indis+1,
1578 num.array([asorted.size])))
1579 bsum = num.zeros(b.size+1)
1580 bsum[1:] = num.cumsum(b)
1581 return (
1582 asorted[indis[:-1]],
1583 (bsum[indis[1:]] - bsum[indis[:-1]]) / (indis[1:]-indis[:-1]))
1586def griddata_regular(x, y, z, xvals, yvals):
1587 nx, ny = xvals.size, yvals.size
1588 xindi = num.digitize(x, centers_to_edges(xvals))
1589 yindi = num.digitize(y, centers_to_edges(yvals))
1591 zindi = yindi*nx+xindi
1592 order = num.argsort(zindi)
1593 z = z[order]
1594 zindi = zindi[order]
1596 zindi, z = blockmean(zindi, z)
1597 znew = num.empty(nx*ny, dtype=float)
1598 znew[:] = num.nan
1599 znew[zindi] = z
1600 return znew.reshape(ny, nx)
1603def guess_field_size(x_sorted, y_sorted, z=None, mode=None):
1604 critical_fraction = 1./num.e - 0.014*3
1605 xs = x_sorted
1606 ys = y_sorted
1607 nxs, nys = nvals(xs), nvals(ys)
1608 if mode == 'nonrandom':
1609 return nxs, nys, 0
1610 elif xs.size == nxs*nys:
1611 # exact match
1612 return nxs, nys, 0
1613 elif nxs >= xs.size*critical_fraction and nys >= xs.size*critical_fraction:
1614 # possibly randomly sampled
1615 nxs = int(math.sqrt(xs.size))
1616 nys = nxs
1617 return nxs, nys, 2
1618 else:
1619 return nxs, nys, 1
1622def griddata_auto(x, y, z, mode=None):
1623 '''
1624 Grid tabular XYZ data by binning.
1626 This function does some extra work to guess the size of the grid. This
1627 should work fine if the input values are already defined on an rectilinear
1628 grid, even if data points are missing or duplicated. This routine also
1629 tries to detect a random distribution of input data and in that case
1630 creates a grid of size sqrt(N) x sqrt(N).
1632 The points do not have to be given in any particular order. Grid nodes
1633 without data are assigned the NaN value. If multiple data points map to the
1634 same grid node, their average is assigned to the grid node.
1635 '''
1637 x, y, z = [num.asarray(X) for X in (x, y, z)]
1638 assert x.size == y.size == z.size
1639 xs, ys = num.sort(x), num.sort(y)
1640 nx, ny, badness = guess_field_size(xs, ys, z, mode=mode)
1641 if badness <= 1:
1642 xf = guess_vals(xs)
1643 yf = guess_vals(ys)
1644 zf = griddata_regular(x, y, z, xf, yf)
1645 else:
1646 xf = num.linspace(xs[0], xs[-1], nx)
1647 yf = num.linspace(ys[0], ys[-1], ny)
1648 zf = griddata_regular(x, y, z, xf, yf)
1650 return xf, yf, zf
1653def tabledata(xf, yf, zf):
1654 assert yf.size, xf.size == zf.shape
1655 x = num.tile(xf, yf.size)
1656 y = num.repeat(yf, xf.size)
1657 z = zf.flatten()
1658 return x, y, z
1661def double1d(a):
1662 a2 = num.empty(a.size*2-1)
1663 a2[::2] = a
1664 a2[1::2] = (a[:-1] + a[1:])/2.
1665 return a2
1668def double2d(f):
1669 f2 = num.empty((f.shape[0]*2-1, f.shape[1]*2-1))
1670 f2[:, :] = num.nan
1671 f2[::2, ::2] = f
1672 f2[1::2, ::2] = (f[:-1, :] + f[1:, :])/2.
1673 f2[::2, 1::2] = (f[:, :-1] + f[:, 1:])/2.
1674 f2[1::2, 1::2] = (f[:-1, :-1] + f[1:, :-1] + f[:-1, 1:] + f[1:, 1:])/4.
1675 diag = f2[1::2, 1::2]
1676 diagA = (f[:-1, :-1] + f[1:, 1:]) / 2.
1677 diagB = (f[1:, :-1] + f[:-1, 1:]) / 2.
1678 f2[1::2, 1::2] = num.where(num.isnan(diag), diagA, diag)
1679 f2[1::2, 1::2] = num.where(num.isnan(diag), diagB, diag)
1680 return f2
1683def doublegrid(x, y, z):
1684 x2 = double1d(x)
1685 y2 = double1d(y)
1686 z2 = double2d(z)
1687 return x2, y2, z2
1690class Guru(object):
1691 '''
1692 Abstract base class providing template interpolation, accessible as
1693 attributes.
1695 Classes deriving from this one, have to implement a :py:meth:`get_params`
1696 method, which is called to get a dict to do ordinary
1697 ``"%(key)x"``-substitutions. The deriving class must also provide a dict
1698 with the templates.
1699 '''
1701 def __init__(self):
1702 self.templates = {}
1704 def get_params(self, ax_projection=False):
1705 '''
1706 To be implemented in subclasses.
1707 '''
1708 raise NotImplementedError
1710 def fill(self, templates, **kwargs):
1711 params = self.get_params(**kwargs)
1712 strings = [t % params for t in templates]
1713 return strings
1715 # hand through templates dict
1716 def __getitem__(self, template_name):
1717 return self.templates[template_name]
1719 def __setitem__(self, template_name, template):
1720 self.templates[template_name] = template
1722 def __contains__(self, template_name):
1723 return template_name in self.templates
1725 def __iter__(self):
1726 return iter(self.templates)
1728 def __len__(self):
1729 return len(self.templates)
1731 def __delitem__(self, template_name):
1732 del self.templates[template_name]
1734 def _simple_fill(self, template_names, **kwargs):
1735 templates = [self.templates[n] for n in template_names]
1736 return self.fill(templates, **kwargs)
1738 def __getattr__(self, template_names):
1739 if [n for n in template_names if n not in self.templates]:
1740 raise AttributeError(template_names)
1742 def f(**kwargs):
1743 return self._simple_fill(template_names, **kwargs)
1745 return f
1748class Ax(AutoScaler):
1749 '''
1750 Ax description with autoscaling capabilities.
1752 The ax is described by the :py:class:`~pyrocko.plot.AutoScaler`
1753 public attributes, plus the following additional attributes
1754 (with default values given in paranthesis):
1756 .. py:attribute:: label
1758 Ax label (without unit).
1760 .. py:attribute:: unit
1762 Physical unit of the data attached to this ax.
1764 .. py:attribute:: scaled_unit
1766 (see below)
1768 .. py:attribute:: scaled_unit_factor
1770 Scaled physical unit and factor between unit and scaled_unit so that
1772 unit = scaled_unit_factor x scaled_unit.
1774 (E.g. if unit is 'm' and data is in the range of nanometers, you may
1775 want to set the scaled_unit to 'nm' and the scaled_unit_factor to
1776 1e9.)
1778 .. py:attribute:: limits
1780 If defined, fix range of ax to limits=(min,max).
1782 .. py:attribute:: masking
1784 If true and if there is a limit on the ax, while calculating ranges,
1785 the data points are masked such that data points outside of this axes
1786 limits are not used to determine the range of another dependant ax.
1788 '''
1790 def __init__(self, label='', unit='', scaled_unit_factor=1.,
1791 scaled_unit='', limits=None, masking=True, **kwargs):
1793 AutoScaler.__init__(self, **kwargs)
1794 self.label = label
1795 self.unit = unit
1796 self.scaled_unit_factor = scaled_unit_factor
1797 self.scaled_unit = scaled_unit
1798 self.limits = limits
1799 self.masking = masking
1801 def label_str(self, exp, unit):
1802 '''
1803 Get label string including the unit and multiplier.
1804 '''
1806 slabel, sunit, sexp = '', '', ''
1807 if self.label:
1808 slabel = self.label
1810 if unit or exp != 0:
1811 if exp != 0:
1812 sexp = '\\327 10@+%i@+' % exp
1813 sunit = '[ %s %s ]' % (sexp, unit)
1814 else:
1815 sunit = '[ %s ]' % unit
1817 p = []
1818 if slabel:
1819 p.append(slabel)
1821 if sunit:
1822 p.append(sunit)
1824 return ' '.join(p)
1826 def make_params(self, data_range, ax_projection=False, override_mode=None,
1827 override_scaled_unit_factor=None):
1829 '''
1830 Get minimum, maximum, increment and label string for ax display.'
1832 Returns minimum, maximum, increment and label string including unit and
1833 multiplier for given data range.
1835 If ``ax_projection`` is True, values suitable to be displayed on the ax
1836 are returned, e.g. min, max and inc are returned in scaled units.
1837 Otherwise the values are returned in the original units, without any
1838 scaling applied.
1839 '''
1841 sf = self.scaled_unit_factor
1843 if override_scaled_unit_factor is not None:
1844 sf = override_scaled_unit_factor
1846 dr_scaled = [sf*x for x in data_range]
1848 mi, ma, inc = self.make_scale(dr_scaled, override_mode=override_mode)
1849 if self.inc is not None:
1850 inc = self.inc*sf
1852 if ax_projection:
1853 exp = self.make_exp(inc)
1854 if sf == 1. and override_scaled_unit_factor is None:
1855 unit = self.unit
1856 else:
1857 unit = self.scaled_unit
1858 label = self.label_str(exp, unit)
1859 return mi/10**exp, ma/10**exp, inc/10**exp, label
1860 else:
1861 label = self.label_str(0, self.unit)
1862 return mi/sf, ma/sf, inc/sf, label
1865class ScaleGuru(Guru):
1867 '''
1868 2D/3D autoscaling and ax annotation facility.
1870 Instances of this class provide automatic determination of plot ranges,
1871 tick increments and scaled annotations, as well as label/unit handling. It
1872 can in particular be used to automatically generate the -R and -B option
1873 arguments, which are required for most GMT commands.
1875 It extends the functionality of the :py:class:`Ax` and
1876 :py:class:`~pyrocko.plot.AutoScaler` classes at the level, where it can not
1877 be handled anymore by looking at a single dimension of the dataset's data,
1878 e.g.:
1880 * The ability to impose a fixed aspect ratio between two axes.
1882 * Recalculation of data range on non-limited axes, when there are
1883 limits imposed on other axes.
1885 '''
1887 def __init__(self, data_tuples=None, axes=None, aspect=None,
1888 percent_interval=None, copy_from=None):
1890 Guru.__init__(self)
1892 if copy_from:
1893 self.templates = copy.deepcopy(copy_from.templates)
1894 self.axes = copy.deepcopy(copy_from.axes)
1895 self.data_ranges = copy.deepcopy(copy_from.data_ranges)
1896 self.aspect = copy_from.aspect
1898 if percent_interval is not None:
1899 from scipy.stats import scoreatpercentile as scap
1901 self.templates = dict(
1902 R='-R%(xmin)g/%(xmax)g/%(ymin)g/%(ymax)g',
1903 B='-B%(xinc)g:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen',
1904 T='-T%(zmin)g/%(zmax)g/%(zinc)g')
1906 maxdim = 2
1907 if data_tuples:
1908 maxdim = max(maxdim, max([len(dt) for dt in data_tuples]))
1909 else:
1910 if axes:
1911 maxdim = len(axes)
1912 data_tuples = [([],) * maxdim]
1913 if axes is not None:
1914 self.axes = axes
1915 else:
1916 self.axes = [Ax() for i in range(maxdim)]
1918 # sophisticated data-range calculation
1919 data_ranges = [None] * maxdim
1920 for dt_ in data_tuples:
1921 dt = num.asarray(dt_)
1922 in_range = True
1923 for ax, x in zip(self.axes, dt):
1924 if ax.limits and ax.masking:
1925 ax_limits = list(ax.limits)
1926 if ax_limits[0] is None:
1927 ax_limits[0] = -num.inf
1928 if ax_limits[1] is None:
1929 ax_limits[1] = num.inf
1930 in_range = num.logical_and(
1931 in_range,
1932 num.logical_and(ax_limits[0] <= x, x <= ax_limits[1]))
1934 for i, ax, x in zip(range(maxdim), self.axes, dt):
1936 if not ax.limits or None in ax.limits:
1937 if len(x) >= 1:
1938 if in_range is not True:
1939 xmasked = num.where(in_range, x, num.NaN)
1940 if percent_interval is None:
1941 range_this = (
1942 num.nanmin(xmasked),
1943 num.nanmax(xmasked))
1944 else:
1945 xmasked_finite = num.compress(
1946 num.isfinite(xmasked), xmasked)
1947 range_this = (
1948 scap(xmasked_finite,
1949 (100.-percent_interval)/2.),
1950 scap(xmasked_finite,
1951 100.-(100.-percent_interval)/2.))
1952 else:
1953 if percent_interval is None:
1954 range_this = num.nanmin(x), num.nanmax(x)
1955 else:
1956 xmasked_finite = num.compress(
1957 num.isfinite(xmasked), xmasked)
1958 range_this = (
1959 scap(xmasked_finite,
1960 (100.-percent_interval)/2.),
1961 scap(xmasked_finite,
1962 100.-(100.-percent_interval)/2.))
1963 else:
1964 range_this = (0., 1.)
1966 if ax.limits:
1967 if ax.limits[0] is not None:
1968 range_this = ax.limits[0], max(ax.limits[0],
1969 range_this[1])
1971 if ax.limits[1] is not None:
1972 range_this = min(ax.limits[1],
1973 range_this[0]), ax.limits[1]
1975 else:
1976 range_this = ax.limits
1978 if data_ranges[i] is None and range_this[0] <= range_this[1]:
1979 data_ranges[i] = range_this
1980 else:
1981 mi, ma = range_this
1982 if data_ranges[i] is not None:
1983 mi = min(data_ranges[i][0], mi)
1984 ma = max(data_ranges[i][1], ma)
1986 data_ranges[i] = (mi, ma)
1988 for i in range(len(data_ranges)):
1989 if data_ranges[i] is None or not (
1990 num.isfinite(data_ranges[i][0])
1991 and num.isfinite(data_ranges[i][1])):
1993 data_ranges[i] = (0., 1.)
1995 self.data_ranges = data_ranges
1996 self.aspect = aspect
1998 def copy(self):
1999 return ScaleGuru(copy_from=self)
2001 def get_params(self, ax_projection=False):
2003 '''
2004 Get dict with output parameters.
2006 For each data dimension, ax minimum, maximum, increment and a label
2007 string (including unit and exponential factor) are determined. E.g. in
2008 for the first dimension the output dict will contain the keys
2009 ``'xmin'``, ``'xmax'``, ``'xinc'``, and ``'xlabel'``.
2011 Normally, values corresponding to the scaling of the raw data are
2012 produced, but if ``ax_projection`` is ``True``, values which are
2013 suitable to be printed on the axes are returned. This means that in the
2014 latter case, the :py:attr:`Ax.scaled_unit` and
2015 :py:attr:`Ax.scaled_unit_factor` attributes as set on the axes are
2016 respected and that a common 10^x factor is factored out and put to the
2017 label string.
2018 '''
2020 xmi, xma, xinc, xlabel = self.axes[0].make_params(
2021 self.data_ranges[0], ax_projection)
2022 ymi, yma, yinc, ylabel = self.axes[1].make_params(
2023 self.data_ranges[1], ax_projection)
2024 if len(self.axes) > 2:
2025 zmi, zma, zinc, zlabel = self.axes[2].make_params(
2026 self.data_ranges[2], ax_projection)
2028 # enforce certain aspect, if needed
2029 if self.aspect is not None:
2030 xwid = xma-xmi
2031 ywid = yma-ymi
2032 if ywid < xwid*self.aspect:
2033 ymi -= (xwid*self.aspect - ywid)*0.5
2034 yma += (xwid*self.aspect - ywid)*0.5
2035 ymi, yma, yinc, ylabel = self.axes[1].make_params(
2036 (ymi, yma), ax_projection, override_mode='off',
2037 override_scaled_unit_factor=1.)
2039 elif xwid < ywid/self.aspect:
2040 xmi -= (ywid/self.aspect - xwid)*0.5
2041 xma += (ywid/self.aspect - xwid)*0.5
2042 xmi, xma, xinc, xlabel = self.axes[0].make_params(
2043 (xmi, xma), ax_projection, override_mode='off',
2044 override_scaled_unit_factor=1.)
2046 params = dict(xmin=xmi, xmax=xma, xinc=xinc, xlabel=xlabel,
2047 ymin=ymi, ymax=yma, yinc=yinc, ylabel=ylabel)
2048 if len(self.axes) > 2:
2049 params.update(dict(zmin=zmi, zmax=zma, zinc=zinc, zlabel=zlabel))
2051 return params
2054class GumSpring(object):
2056 '''
2057 Sizing policy implementing a minimal size, plus a desire to grow.
2058 '''
2060 def __init__(self, minimal=None, grow=None):
2061 self.minimal = minimal
2062 if grow is None:
2063 if minimal is None:
2064 self.grow = 1.0
2065 else:
2066 self.grow = 0.0
2067 else:
2068 self.grow = grow
2069 self.value = 1.0
2071 def get_minimal(self):
2072 if self.minimal is not None:
2073 return self.minimal
2074 else:
2075 return 0.0
2077 def get_grow(self):
2078 return self.grow
2080 def set_value(self, value):
2081 self.value = value
2083 def get_value(self):
2084 return self.value
2087def distribute(sizes, grows, space):
2088 sizes = list(sizes)
2089 gsum = sum(grows)
2090 if gsum > 0.0:
2091 for i in range(len(sizes)):
2092 sizes[i] += space*grows[i]/gsum
2093 return sizes
2096class Widget(Guru):
2098 '''
2099 Base class of the gmtpy layout system.
2101 The Widget class provides the basic functionality for the nesting and
2102 placing of elements on the output page, and maintains the sizing policies
2103 of each element. Each of the layouts defined in gmtpy is itself a Widget.
2105 Sizing of the widget is controlled by :py:meth:`get_min_size` and
2106 :py:meth:`get_grow` which should be overloaded in derived classes. The
2107 basic behaviour of a Widget instance is to have a vertical and a horizontal
2108 minimum size which default to zero, as well as a vertical and a horizontal
2109 desire to grow, represented by floats, which default to 1.0. Additionally
2110 an aspect ratio constraint may be imposed on the Widget.
2112 After layouting, the widget provides its width, height, x-offset and
2113 y-offset in various ways. Via the Guru interface (see :py:class:`Guru`
2114 class), templates for the -X, -Y and -J option arguments used by GMT
2115 arguments are provided. The defaults are suitable for plotting of linear
2116 (-JX) plots. Other projections can be selected by giving an appropriate 'J'
2117 template, or by manual construction of the -J option, e.g. by utilizing the
2118 :py:meth:`width` and :py:meth:`height` methods. The :py:meth:`bbox` method
2119 can be used to create a PostScript bounding box from the widgets border,
2120 e.g. for use in :py:meth:`GMT.save`.
2122 The convention is, that all sizes are given in PostScript points.
2123 Conversion factors are provided as constants :py:const:`inch` and
2124 :py:const:`cm` in the gmtpy module.
2125 '''
2127 def __init__(self, horizontal=None, vertical=None, parent=None):
2129 '''
2130 Create new widget.
2131 '''
2133 Guru.__init__(self)
2135 self.templates = dict(
2136 X='-Xa%(xoffset)gp',
2137 Y='-Ya%(yoffset)gp',
2138 J='-JX%(width)gp/%(height)gp')
2140 if horizontal is None:
2141 self.horizontal = GumSpring()
2142 else:
2143 self.horizontal = horizontal
2145 if vertical is None:
2146 self.vertical = GumSpring()
2147 else:
2148 self.vertical = vertical
2150 self.aspect = None
2151 self.parent = parent
2152 self.dirty = True
2154 def set_widget(self):
2155 '''
2156 To be implemented in subclasses.
2157 '''
2158 raise NotImplementedError
2160 def set_parent(self, parent):
2162 '''
2163 Set the parent widget.
2165 This method should not be called directly. The :py:meth:`set_widget`
2166 methods are responsible for calling this.
2167 '''
2169 self.parent = parent
2170 self.dirtyfy()
2172 def get_parent(self):
2174 '''
2175 Get the widgets parent widget.
2176 '''
2178 return self.parent
2180 def get_root(self):
2182 '''
2183 Get the root widget in the layout hierarchy.
2184 '''
2186 if self.parent is not None:
2187 return self.get_parent()
2188 else:
2189 return self
2191 def set_horizontal(self, minimal=None, grow=None):
2193 '''
2194 Set the horizontal sizing policy of the Widget.
2197 :param minimal: new minimal width of the widget
2198 :param grow: new horizontal grow disire of the widget
2199 '''
2201 self.horizontal = GumSpring(minimal, grow)
2202 self.dirtyfy()
2204 def get_horizontal(self):
2205 return self.horizontal.get_minimal(), self.horizontal.get_grow()
2207 def set_vertical(self, minimal=None, grow=None):
2209 '''
2210 Set the horizontal sizing policy of the Widget.
2212 :param minimal: new minimal height of the widget
2213 :param grow: new vertical grow disire of the widget
2214 '''
2216 self.vertical = GumSpring(minimal, grow)
2217 self.dirtyfy()
2219 def get_vertical(self):
2220 return self.vertical.get_minimal(), self.vertical.get_grow()
2222 def set_aspect(self, aspect=None):
2224 '''
2225 Set aspect constraint on the widget.
2227 The aspect is given as height divided by width.
2228 '''
2230 self.aspect = aspect
2231 self.dirtyfy()
2233 def set_policy(self, minimal=(None, None), grow=(None, None), aspect=None):
2235 '''
2236 Shortcut to set sizing and aspect constraints in a single method
2237 call.
2238 '''
2240 self.set_horizontal(minimal[0], grow[0])
2241 self.set_vertical(minimal[1], grow[1])
2242 self.set_aspect(aspect)
2244 def get_policy(self):
2245 mh, gh = self.get_horizontal()
2246 mv, gv = self.get_vertical()
2247 return (mh, mv), (gh, gv), self.aspect
2249 def legalize(self, size, offset):
2251 '''
2252 Get legal size for widget.
2254 Returns: (new_size, new_offset)
2256 Given a box as ``size`` and ``offset``, return ``new_size`` and
2257 ``new_offset``, such that the widget's sizing and aspect constraints
2258 are fullfilled. The returned box is centered on the given input box.
2259 '''
2261 sh, sv = size
2262 oh, ov = offset
2263 shs, svs = Widget.get_min_size(self)
2264 ghs, gvs = Widget.get_grow(self)
2266 if ghs == 0.0:
2267 oh += (sh-shs)/2.
2268 sh = shs
2270 if gvs == 0.0:
2271 ov += (sv-svs)/2.
2272 sv = svs
2274 if self.aspect is not None:
2275 if sh > sv/self.aspect:
2276 oh += (sh-sv/self.aspect)/2.
2277 sh = sv/self.aspect
2278 if sv > sh*self.aspect:
2279 ov += (sv-sh*self.aspect)/2.
2280 sv = sh*self.aspect
2282 return (sh, sv), (oh, ov)
2284 def get_min_size(self):
2286 '''
2287 Get minimum size of widget.
2289 Used by the layout managers. Should be overloaded in derived classes.
2290 '''
2292 mh, mv = self.horizontal.get_minimal(), self.vertical.get_minimal()
2293 if self.aspect is not None:
2294 if mv == 0.0:
2295 return mh, mh*self.aspect
2296 elif mh == 0.0:
2297 return mv/self.aspect, mv
2298 return mh, mv
2300 def get_grow(self):
2302 '''
2303 Get widget's desire to grow.
2305 Used by the layout managers. Should be overloaded in derived classes.
2306 '''
2308 return self.horizontal.get_grow(), self.vertical.get_grow()
2310 def set_size(self, size, offset):
2312 '''
2313 Set the widget's current size.
2315 Should not be called directly. It is the layout manager's
2316 responsibility to call this.
2317 '''
2319 (sh, sv), inner_offset = self.legalize(size, offset)
2320 self.offset = inner_offset
2321 self.horizontal.set_value(sh)
2322 self.vertical.set_value(sv)
2323 self.dirty = False
2325 def __str__(self):
2327 def indent(ind, str):
2328 return ('\n'+ind).join(str.splitlines())
2329 size, offset = self.get_size()
2330 s = '%s (%g x %g) (%g, %g)\n' % ((self.__class__,) + size + offset)
2331 children = self.get_children()
2332 if children:
2333 s += '\n'.join([' ' + indent(' ', str(c)) for c in children])
2334 return s
2336 def policies_debug_str(self):
2338 def indent(ind, str):
2339 return ('\n'+ind).join(str.splitlines())
2340 mins, grows, aspect = self.get_policy()
2341 s = '%s: minimum=(%s, %s), grow=(%s, %s), aspect=%s\n' % (
2342 (self.__class__,) + mins+grows+(aspect,))
2344 children = self.get_children()
2345 if children:
2346 s += '\n'.join([' ' + indent(
2347 ' ', c.policies_debug_str()) for c in children])
2348 return s
2350 def get_corners(self, descend=False):
2352 '''
2353 Get coordinates of the corners of the widget.
2355 Returns list with coordinate tuples.
2357 If ``descend`` is True, the returned list will contain corner
2358 coordinates of all sub-widgets.
2359 '''
2361 self.do_layout()
2362 (sh, sv), (oh, ov) = self.get_size()
2363 corners = [(oh, ov), (oh+sh, ov), (oh+sh, ov+sv), (oh, ov+sv)]
2364 if descend:
2365 for child in self.get_children():
2366 corners.extend(child.get_corners(descend=True))
2367 return corners
2369 def get_sizes(self):
2371 '''
2372 Get sizes of this widget and all it's children.
2374 Returns a list with size tuples.
2375 '''
2376 self.do_layout()
2377 sizes = [self.get_size()]
2378 for child in self.get_children():
2379 sizes.extend(child.get_sizes())
2380 return sizes
2382 def do_layout(self):
2384 '''
2385 Triggers layouting of the widget hierarchy, if needed.
2386 '''
2388 if self.parent is not None:
2389 return self.parent.do_layout()
2391 if not self.dirty:
2392 return
2394 sh, sv = self.get_min_size()
2395 gh, gv = self.get_grow()
2396 if sh == 0.0 and gh != 0.0:
2397 sh = 15.*cm
2398 if sv == 0.0 and gv != 0.0:
2399 sv = 15.*cm*gv/gh * 1./golden_ratio
2400 self.set_size((sh, sv), (0., 0.))
2402 def get_children(self):
2404 '''
2405 Get sub-widgets contained in this widget.
2407 Returns a list of widgets.
2408 '''
2410 return []
2412 def get_size(self):
2414 '''
2415 Get current size and position of the widget.
2417 Triggers layouting and returns
2418 ``((width, height), (xoffset, yoffset))``
2419 '''
2421 self.do_layout()
2422 return (self.horizontal.get_value(),
2423 self.vertical.get_value()), self.offset
2425 def get_params(self):
2427 '''
2428 Get current size and position of the widget.
2430 Triggers layouting and returns dict with keys ``'xoffset'``,
2431 ``'yoffset'``, ``'width'`` and ``'height'``.
2432 '''
2434 self.do_layout()
2435 (w, h), (xo, yo) = self.get_size()
2436 return dict(xoffset=xo, yoffset=yo, width=w, height=h,
2437 width_m=w/_units['m'])
2439 def width(self):
2441 '''
2442 Get current width of the widget.
2444 Triggers layouting and returns width.
2445 '''
2447 self.do_layout()
2448 return self.horizontal.get_value()
2450 def height(self):
2452 '''
2453 Get current height of the widget.
2455 Triggers layouting and return height.
2456 '''
2458 self.do_layout()
2459 return self.vertical.get_value()
2461 def bbox(self):
2463 '''
2464 Get PostScript bounding box for this widget.
2466 Triggers layouting and returns values suitable to create PS bounding
2467 box, representing the widgets current size and position.
2468 '''
2470 self.do_layout()
2471 return (self.offset[0], self.offset[1], self.offset[0]+self.width(),
2472 self.offset[1]+self.height())
2474 def dirtyfy(self):
2476 '''
2477 Set dirty flag on top level widget in the hierarchy.
2479 Called by various methods, to indicate, that the widget hierarchy needs
2480 new layouting.
2481 '''
2483 if self.parent is not None:
2484 self.parent.dirtyfy()
2486 self.dirty = True
2489class CenterLayout(Widget):
2491 '''
2492 A layout manager which centers its single child widget.
2494 The child widget may be oversized.
2495 '''
2497 def __init__(self, horizontal=None, vertical=None):
2498 Widget.__init__(self, horizontal, vertical)
2499 self.content = Widget(horizontal=GumSpring(grow=1.),
2500 vertical=GumSpring(grow=1.), parent=self)
2502 def get_min_size(self):
2503 shs, svs = Widget.get_min_size(self)
2504 sh, sv = self.content.get_min_size()
2505 return max(shs, sh), max(svs, sv)
2507 def get_grow(self):
2508 ghs, gvs = Widget.get_grow(self)
2509 gh, gv = self.content.get_grow()
2510 return gh*ghs, gv*gvs
2512 def set_size(self, size, offset):
2513 (sh, sv), (oh, ov) = self.legalize(size, offset)
2515 shc, svc = self.content.get_min_size()
2516 ghc, gvc = self.content.get_grow()
2517 if ghc != 0.:
2518 shc = sh
2519 if gvc != 0.:
2520 svc = sv
2521 ohc = oh+(sh-shc)/2.
2522 ovc = ov+(sv-svc)/2.
2524 self.content.set_size((shc, svc), (ohc, ovc))
2525 Widget.set_size(self, (sh, sv), (oh, ov))
2527 def set_widget(self, widget=None):
2529 '''
2530 Set the child widget, which shall be centered.
2531 '''
2533 if widget is None:
2534 widget = Widget()
2536 self.content = widget
2538 widget.set_parent(self)
2540 def get_widget(self):
2541 return self.content
2543 def get_children(self):
2544 return [self.content]
2547class FrameLayout(Widget):
2549 '''
2550 A layout manager containing a center widget sorrounded by four margin
2551 widgets.
2553 ::
2555 +---------------------------+
2556 | top |
2557 +---------------------------+
2558 | | | |
2559 | left | center | right |
2560 | | | |
2561 +---------------------------+
2562 | bottom |
2563 +---------------------------+
2565 This layout manager does a little bit of extra effort to maintain the
2566 aspect constraint of the center widget, if this is set. It does so, by
2567 allowing for a bit more flexibility in the sizing of the margins. Two
2568 shortcut methods are provided to set the margin sizes in one shot:
2569 :py:meth:`set_fixed_margins` and :py:meth:`set_min_margins`. The first sets
2570 the margins to fixed sizes, while the second gives them a minimal size and
2571 a (neglectably) small desire to grow. Using the latter may be useful when
2572 setting an aspect constraint on the center widget, because this way the
2573 maximum size of the center widget may be controlled without creating empty
2574 spaces between the widgets.
2575 '''
2577 def __init__(self, horizontal=None, vertical=None):
2578 Widget.__init__(self, horizontal, vertical)
2579 mw = 3.*cm
2580 self.left = Widget(
2581 horizontal=GumSpring(grow=0.15, minimal=mw), parent=self)
2582 self.right = Widget(
2583 horizontal=GumSpring(grow=0.15, minimal=mw), parent=self)
2584 self.top = Widget(
2585 vertical=GumSpring(grow=0.15, minimal=mw/golden_ratio),
2586 parent=self)
2587 self.bottom = Widget(
2588 vertical=GumSpring(grow=0.15, minimal=mw/golden_ratio),
2589 parent=self)
2590 self.center = Widget(
2591 horizontal=GumSpring(grow=0.7), vertical=GumSpring(grow=0.7),
2592 parent=self)
2594 def set_fixed_margins(self, left, right, top, bottom):
2595 '''
2596 Give margins fixed size constraints.
2597 '''
2599 self.left.set_horizontal(left, 0)
2600 self.right.set_horizontal(right, 0)
2601 self.top.set_vertical(top, 0)
2602 self.bottom.set_vertical(bottom, 0)
2604 def set_min_margins(self, left, right, top, bottom, grow=0.0001):
2605 '''
2606 Give margins a minimal size and the possibility to grow.
2608 The desire to grow is set to a very small number.
2609 '''
2610 self.left.set_horizontal(left, grow)
2611 self.right.set_horizontal(right, grow)
2612 self.top.set_vertical(top, grow)
2613 self.bottom.set_vertical(bottom, grow)
2615 def get_min_size(self):
2616 shs, svs = Widget.get_min_size(self)
2618 sl, sr, st, sb, sc = [x.get_min_size() for x in (
2619 self.left, self.right, self.top, self.bottom, self.center)]
2620 gl, gr, gt, gb, gc = [x.get_grow() for x in (
2621 self.left, self.right, self.top, self.bottom, self.center)]
2623 shsum = sl[0]+sr[0]+sc[0]
2624 svsum = st[1]+sb[1]+sc[1]
2626 # prevent widgets from collapsing
2627 for s, g in ((sl, gl), (sr, gr), (sc, gc)):
2628 if s[0] == 0.0 and g[0] != 0.0:
2629 shsum += 0.1*cm
2631 for s, g in ((st, gt), (sb, gb), (sc, gc)):
2632 if s[1] == 0.0 and g[1] != 0.0:
2633 svsum += 0.1*cm
2635 sh = max(shs, shsum)
2636 sv = max(svs, svsum)
2638 return sh, sv
2640 def get_grow(self):
2641 ghs, gvs = Widget.get_grow(self)
2642 gh = (self.left.get_grow()[0] +
2643 self.right.get_grow()[0] +
2644 self.center.get_grow()[0]) * ghs
2645 gv = (self.top.get_grow()[1] +
2646 self.bottom.get_grow()[1] +
2647 self.center.get_grow()[1]) * gvs
2648 return gh, gv
2650 def set_size(self, size, offset):
2651 (sh, sv), (oh, ov) = self.legalize(size, offset)
2653 sl, sr, st, sb, sc = [x.get_min_size() for x in (
2654 self.left, self.right, self.top, self.bottom, self.center)]
2655 gl, gr, gt, gb, gc = [x.get_grow() for x in (
2656 self.left, self.right, self.top, self.bottom, self.center)]
2658 ah = sh - (sl[0]+sr[0]+sc[0])
2659 av = sv - (st[1]+sb[1]+sc[1])
2661 if ah < 0.0:
2662 raise GmtPyError('Container not wide enough for contents '
2663 '(FrameLayout, available: %g cm, needed: %g cm)'
2664 % (sh/cm, (sl[0]+sr[0]+sc[0])/cm))
2665 if av < 0.0:
2666 raise GmtPyError('Container not high enough for contents '
2667 '(FrameLayout, available: %g cm, needed: %g cm)'
2668 % (sv/cm, (st[1]+sb[1]+sc[1])/cm))
2670 slh, srh, sch = distribute((sl[0], sr[0], sc[0]),
2671 (gl[0], gr[0], gc[0]), ah)
2672 stv, sbv, scv = distribute((st[1], sb[1], sc[1]),
2673 (gt[1], gb[1], gc[1]), av)
2675 if self.center.aspect is not None:
2676 ahm = sh - (sl[0]+sr[0] + scv/self.center.aspect)
2677 avm = sv - (st[1]+sb[1] + sch*self.center.aspect)
2678 if 0.0 < ahm < ah:
2679 slh, srh, sch = distribute(
2680 (sl[0], sr[0], scv/self.center.aspect),
2681 (gl[0], gr[0], 0.0), ahm)
2683 elif 0.0 < avm < av:
2684 stv, sbv, scv = distribute((st[1], sb[1],
2685 sch*self.center.aspect),
2686 (gt[1], gb[1], 0.0), avm)
2688 ah = sh - (slh+srh+sch)
2689 av = sv - (stv+sbv+scv)
2691 oh += ah/2.
2692 ov += av/2.
2693 sh -= ah
2694 sv -= av
2696 self.left.set_size((slh, scv), (oh, ov+sbv))
2697 self.right.set_size((srh, scv), (oh+slh+sch, ov+sbv))
2698 self.top.set_size((sh, stv), (oh, ov+sbv+scv))
2699 self.bottom.set_size((sh, sbv), (oh, ov))
2700 self.center.set_size((sch, scv), (oh+slh, ov+sbv))
2701 Widget.set_size(self, (sh, sv), (oh, ov))
2703 def set_widget(self, which='center', widget=None):
2705 '''
2706 Set one of the sub-widgets.
2708 ``which`` should be one of ``'left'``, ``'right'``, ``'top'``,
2709 ``'bottom'`` or ``'center'``.
2710 '''
2712 if widget is None:
2713 widget = Widget()
2715 if which in ('left', 'right', 'top', 'bottom', 'center'):
2716 self.__dict__[which] = widget
2717 else:
2718 raise GmtPyError('No such sub-widget: %s' % which)
2720 widget.set_parent(self)
2722 def get_widget(self, which='center'):
2724 '''
2725 Get one of the sub-widgets.
2727 ``which`` should be one of ``'left'``, ``'right'``, ``'top'``,
2728 ``'bottom'`` or ``'center'``.
2729 '''
2731 if which in ('left', 'right', 'top', 'bottom', 'center'):
2732 return self.__dict__[which]
2733 else:
2734 raise GmtPyError('No such sub-widget: %s' % which)
2736 def get_children(self):
2737 return [self.left, self.right, self.top, self.bottom, self.center]
2740class GridLayout(Widget):
2742 '''
2743 A layout manager which arranges its sub-widgets in a grid.
2745 The grid spacing is flexible and based on the sizing policies of the
2746 contained sub-widgets. If an equidistant grid is needed, the sizing
2747 policies of the sub-widgets have to be set equally.
2749 The height of each row and the width of each column is derived from the
2750 sizing policy of the largest sub-widget in the row or column in question.
2751 The algorithm is not very sophisticated, so conflicting sizing policies
2752 might not be resolved optimally.
2753 '''
2755 def __init__(self, nx=2, ny=2, horizontal=None, vertical=None):
2757 '''
2758 Create new grid layout with ``nx`` columns and ``ny`` rows.
2759 '''
2761 Widget.__init__(self, horizontal, vertical)
2762 self.grid = []
2763 for iy in range(ny):
2764 row = []
2765 for ix in range(nx):
2766 w = Widget(parent=self)
2767 row.append(w)
2769 self.grid.append(row)
2771 def sub_min_sizes_as_array(self):
2772 esh = num.array(
2773 [[w.get_min_size()[0] for w in row] for row in self.grid],
2774 dtype=float)
2775 esv = num.array(
2776 [[w.get_min_size()[1] for w in row] for row in self.grid],
2777 dtype=float)
2778 return esh, esv
2780 def sub_grows_as_array(self):
2781 egh = num.array(
2782 [[w.get_grow()[0] for w in row] for row in self.grid],
2783 dtype=float)
2784 egv = num.array(
2785 [[w.get_grow()[1] for w in row] for row in self.grid],
2786 dtype=float)
2787 return egh, egv
2789 def get_min_size(self):
2790 sh, sv = Widget.get_min_size(self)
2791 esh, esv = self.sub_min_sizes_as_array()
2792 if esh.size != 0:
2793 sh = max(sh, num.sum(esh.max(0)))
2794 if esv.size != 0:
2795 sv = max(sv, num.sum(esv.max(1)))
2796 return sh, sv
2798 def get_grow(self):
2799 ghs, gvs = Widget.get_grow(self)
2800 egh, egv = self.sub_grows_as_array()
2801 if egh.size != 0:
2802 gh = num.sum(egh.max(0))*ghs
2803 else:
2804 gh = 1.0
2805 if egv.size != 0:
2806 gv = num.sum(egv.max(1))*gvs
2807 else:
2808 gv = 1.0
2809 return gh, gv
2811 def set_size(self, size, offset):
2812 (sh, sv), (oh, ov) = self.legalize(size, offset)
2813 esh, esv = self.sub_min_sizes_as_array()
2814 egh, egv = self.sub_grows_as_array()
2816 # available additional space
2817 empty = esh.size == 0
2819 if not empty:
2820 ah = sh - num.sum(esh.max(0))
2821 av = sv - num.sum(esv.max(1))
2822 else:
2823 av = sv
2824 ah = sh
2826 if ah < 0.0:
2827 raise GmtPyError('Container not wide enough for contents '
2828 '(GridLayout, available: %g cm, needed: %g cm)'
2829 % (sh/cm, (num.sum(esh.max(0)))/cm))
2830 if av < 0.0:
2831 raise GmtPyError('Container not high enough for contents '
2832 '(GridLayout, available: %g cm, needed: %g cm)'
2833 % (sv/cm, (num.sum(esv.max(1)))/cm))
2835 nx, ny = esh.shape
2837 if not empty:
2838 # distribute additional space on rows and columns
2839 # according to grow weights and minimal sizes
2840 gsh = egh.sum(1)[:, num.newaxis].repeat(ny, axis=1)
2841 nesh = esh.copy()
2842 nesh += num.where(gsh > 0.0, ah*egh/gsh, 0.0)
2844 nsh = num.maximum(nesh.max(0), esh.max(0))
2846 gsv = egv.sum(0)[num.newaxis, :].repeat(nx, axis=0)
2847 nesv = esv.copy()
2848 nesv += num.where(gsv > 0.0, av*egv/gsv, 0.0)
2849 nsv = num.maximum(nesv.max(1), esv.max(1))
2851 ah = sh - sum(nsh)
2852 av = sv - sum(nsv)
2854 oh += ah/2.
2855 ov += av/2.
2856 sh -= ah
2857 sv -= av
2859 # resize child widgets
2860 neov = ov + sum(nsv)
2861 for row, nesv in zip(self.grid, nsv):
2862 neov -= nesv
2863 neoh = oh
2864 for w, nesh in zip(row, nsh):
2865 w.set_size((nesh, nesv), (neoh, neov))
2866 neoh += nesh
2868 Widget.set_size(self, (sh, sv), (oh, ov))
2870 def set_widget(self, ix, iy, widget=None):
2872 '''
2873 Set one of the sub-widgets.
2875 Sets the sub-widget in column ``ix`` and row ``iy``. The indices are
2876 counted from zero.
2877 '''
2879 if widget is None:
2880 widget = Widget()
2882 self.grid[iy][ix] = widget
2883 widget.set_parent(self)
2885 def get_widget(self, ix, iy):
2887 '''
2888 Get one of the sub-widgets.
2890 Gets the sub-widget from column ``ix`` and row ``iy``. The indices are
2891 counted from zero.
2892 '''
2894 return self.grid[iy][ix]
2896 def get_children(self):
2897 children = []
2898 for row in self.grid:
2899 children.extend(row)
2901 return children
2904def is_gmt5(version='newest'):
2905 return get_gmt_installation(version)['version'][0] in ['5', '6']
2908def is_gmt6(version='newest'):
2909 return get_gmt_installation(version)['version'][0] in ['6']
2912def aspect_for_projection(gmtversion, *args, **kwargs):
2914 gmt = GMT(version=gmtversion, eps_mode=True)
2916 if gmt.is_gmt5():
2917 gmt.psbasemap('-B+gblack', finish=True, *args, **kwargs)
2918 fn = gmt.tempfilename('test.eps')
2919 gmt.save(fn, crop_eps_mode=True)
2920 with open(fn, 'rb') as f:
2921 s = f.read()
2923 l, b, r, t = get_bbox(s) # noqa
2924 else:
2925 gmt.psbasemap('-G0', finish=True, *args, **kwargs)
2926 l, b, r, t = gmt.bbox() # noqa
2928 return (t-b)/(r-l) # noqa
2931def text_box(
2932 text, font=0, font_size=12., angle=0, gmtversion='newest', **kwargs):
2934 gmt = GMT(version=gmtversion)
2935 if gmt.is_gmt5():
2936 row = [0, 0, text]
2937 farg = ['-F+f%gp,%s,%s+j%s' % (font_size, font, 'black', 'BL')]
2938 else:
2939 row = [0, 0, font_size, 0, font, 'BL', text]
2940 farg = []
2942 gmt.pstext(
2943 in_rows=[row],
2944 finish=True,
2945 R=(0, 1, 0, 1),
2946 J='x10p',
2947 N=True,
2948 *farg,
2949 **kwargs)
2951 fn = gmt.tempfilename() + '.ps'
2952 gmt.save(fn)
2954 (_, stderr) = subprocess.Popen(
2955 ['gs', '-q', '-dNOPAUSE', '-dBATCH', '-r720', '-sDEVICE=bbox', fn],
2956 stderr=subprocess.PIPE).communicate()
2958 dx, dy = None, None
2959 for line in stderr.splitlines():
2960 if line.startswith(b'%%HiResBoundingBox:'):
2961 l, b, r, t = [float(x) for x in line.split()[-4:]] # noqa
2962 dx, dy = r-l, t-b # noqa
2963 break
2965 return dx, dy
2968class TableLiner(object):
2969 '''
2970 Utility class to turn tables into lines.
2971 '''
2973 def __init__(self, in_columns=None, in_rows=None, encoding='utf-8'):
2974 self.in_columns = in_columns
2975 self.in_rows = in_rows
2976 self.encoding = encoding
2978 def __iter__(self):
2979 if self.in_columns is not None:
2980 for row in zip(*self.in_columns):
2981 yield (' '.join([str(x) for x in row])+'\n').encode(
2982 self.encoding)
2984 if self.in_rows is not None:
2985 for row in self.in_rows:
2986 yield (' '.join([str(x) for x in row])+'\n').encode(
2987 self.encoding)
2990class LineStreamChopper(object):
2991 '''
2992 File-like object to buffer data.
2993 '''
2995 def __init__(self, liner):
2996 self.chopsize = None
2997 self.liner = liner
2998 self.chop_iterator = None
2999 self.closed = False
3001 def _chopiter(self):
3002 buf = BytesIO()
3003 for line in self.liner:
3004 buf.write(line)
3005 buflen = buf.tell()
3006 if self.chopsize is not None and buflen >= self.chopsize:
3007 buf.seek(0)
3008 while buf.tell() <= buflen-self.chopsize:
3009 yield buf.read(self.chopsize)
3011 newbuf = BytesIO()
3012 newbuf.write(buf.read())
3013 buf.close()
3014 buf = newbuf
3016 yield buf.getvalue()
3017 buf.close()
3019 def read(self, size=None):
3020 if self.closed:
3021 raise ValueError('Cannot read from closed LineStreamChopper.')
3022 if self.chop_iterator is None:
3023 self.chopsize = size
3024 self.chop_iterator = self._chopiter()
3026 self.chopsize = size
3027 try:
3028 return next(self.chop_iterator)
3029 except StopIteration:
3030 return ''
3032 def close(self):
3033 self.chopsize = None
3034 self.chop_iterator = None
3035 self.closed = True
3037 def flush(self):
3038 pass
3041font_tab = {
3042 0: 'Helvetica',
3043 1: 'Helvetica-Bold',
3044}
3046font_tab_rev = dict((v, k) for (k, v) in font_tab.items())
3049class GMT(object):
3050 '''
3051 A thin wrapper to GMT command execution.
3053 A dict ``config`` may be given to override some of the default GMT
3054 parameters. The ``version`` argument may be used to select a specific GMT
3055 version, which should be used with this GMT instance. The selected
3056 version of GMT has to be installed on the system, must be supported by
3057 gmtpy and gmtpy must know where to find it.
3059 Each instance of this class is used for the task of producing one PS or PDF
3060 output file.
3062 Output of a series of GMT commands is accumulated in memory and can then be
3063 saved as PS or PDF file using the :py:meth:`save` method.
3065 GMT commands are accessed as method calls to instances of this class. See
3066 the :py:meth:`__getattr__` method for details on how the method's
3067 arguments are translated into options and arguments for the GMT command.
3069 Associated with each instance of this class, a temporary directory is
3070 created, where temporary files may be created, and which is automatically
3071 deleted, when the object is destroyed. The :py:meth:`tempfilename` method
3072 may be used to get a random filename in the instance's temporary directory.
3074 Any .gmtdefaults files are ignored. The GMT class uses a fixed
3075 set of defaults, which may be altered via an argument to the constructor.
3076 If possible, GMT is run in 'isolation mode', which was introduced with GMT
3077 version 4.2.2, by setting `GMT_TMPDIR` to the instance's temporary
3078 directory. With earlier versions of GMT, problems may arise with parallel
3079 execution of more than one GMT instance.
3081 Each instance of the GMT class may pick a specific version of GMT which
3082 shall be used, so that, if multiple versions of GMT are installed on the
3083 system, different versions of GMT can be used simultaneously such that
3084 backward compatibility of the scripts can be maintained.
3086 '''
3088 def __init__(
3089 self,
3090 config=None,
3091 kontinue=None,
3092 version='newest',
3093 config_papersize=None,
3094 eps_mode=False):
3096 self.installation = get_gmt_installation(version)
3097 self.gmt_config = dict(self.installation['defaults'])
3098 self.eps_mode = eps_mode
3099 self._shutil = shutil
3101 if config:
3102 self.gmt_config.update(config)
3104 if config_papersize:
3105 if not isinstance(config_papersize, str):
3106 config_papersize = 'Custom_%ix%i' % (
3107 int(config_papersize[0]), int(config_papersize[1]))
3109 if self.is_gmt5():
3110 self.gmt_config['PS_MEDIA'] = config_papersize
3111 else:
3112 self.gmt_config['PAPER_MEDIA'] = config_papersize
3114 self.tempdir = tempfile.mkdtemp('', 'gmtpy-')
3115 self.gmt_config_filename = pjoin(self.tempdir, 'gmt.conf')
3116 self.gen_gmt_config_file(self.gmt_config_filename, self.gmt_config)
3118 if kontinue is not None:
3119 self.load_unfinished(kontinue)
3120 self.needstart = False
3121 else:
3122 self.output = BytesIO()
3123 self.needstart = True
3125 self.finished = False
3127 self.environ = os.environ.copy()
3128 self.environ['GMTHOME'] = self.installation.get('home', '')
3129 # GMT isolation mode: works only properly with GMT version >= 4.2.2
3130 self.environ['GMT_TMPDIR'] = self.tempdir
3132 self.layout = None
3133 self.command_log = []
3134 self.keep_temp_dir = False
3136 def is_gmt5(self):
3137 return self.get_version()[0] in ['5', '6']
3139 def is_gmt6(self):
3140 return self.get_version()[0] in ['6']
3142 def get_version(self):
3143 return self.installation['version']
3145 def get_config(self, key):
3146 return self.gmt_config[key]
3148 def to_points(self, string):
3149 if not string:
3150 return 0
3152 unit = string[-1]
3153 if unit in _units:
3154 return float(string[:-1])/_units[unit]
3155 else:
3156 default_unit = measure_unit(self.gmt_config).lower()[0]
3157 return float(string)/_units[default_unit]
3159 def label_font_size(self):
3160 if self.is_gmt5():
3161 return self.to_points(self.gmt_config['FONT_LABEL'].split(',')[0])
3162 else:
3163 return self.to_points(self.gmt_config['LABEL_FONT_SIZE'])
3165 def label_font(self):
3166 if self.is_gmt5():
3167 return font_tab_rev(self.gmt_config['FONT_LABEL'].split(',')[1])
3168 else:
3169 return self.gmt_config['LABEL_FONT']
3171 def gen_gmt_config_file(self, config_filename, config):
3172 f = open(config_filename, 'wb')
3173 f.write(
3174 ('#\n# GMT %s Defaults file\n'
3175 % self.installation['version']).encode('ascii'))
3177 for k, v in config.items():
3178 f.write(('%s = %s\n' % (k, v)).encode('ascii'))
3179 f.close()
3181 def __del__(self):
3182 if not self.keep_temp_dir:
3183 self._shutil.rmtree(self.tempdir)
3185 def _gmtcommand(self, command, *addargs, **kwargs):
3187 '''
3188 Execute arbitrary GMT command.
3190 See docstring in __getattr__ for details.
3191 '''
3193 in_stream = kwargs.pop('in_stream', None)
3194 in_filename = kwargs.pop('in_filename', None)
3195 in_string = kwargs.pop('in_string', None)
3196 in_columns = kwargs.pop('in_columns', None)
3197 in_rows = kwargs.pop('in_rows', None)
3198 out_stream = kwargs.pop('out_stream', None)
3199 out_filename = kwargs.pop('out_filename', None)
3200 out_discard = kwargs.pop('out_discard', None)
3201 finish = kwargs.pop('finish', False)
3202 suppressdefaults = kwargs.pop('suppress_defaults', False)
3203 config_override = kwargs.pop('config', None)
3205 assert not self.finished
3207 # check for mutual exclusiveness on input and output possibilities
3208 assert (1 >= len(
3209 [x for x in [
3210 in_stream, in_filename, in_string, in_columns, in_rows]
3211 if x is not None]))
3212 assert (1 >= len([x for x in [out_stream, out_filename, out_discard]
3213 if x is not None]))
3215 options = []
3217 gmt_config = self.gmt_config
3218 if not self.is_gmt5():
3219 gmt_config_filename = self.gmt_config_filename
3220 if config_override:
3221 gmt_config = self.gmt_config.copy()
3222 gmt_config.update(config_override)
3223 gmt_config_override_filename = pjoin(
3224 self.tempdir, 'gmtdefaults_override')
3225 self.gen_gmt_config_file(
3226 gmt_config_override_filename, gmt_config)
3227 gmt_config_filename = gmt_config_override_filename
3229 else: # gmt5 needs override variables as --VAR=value
3230 if config_override:
3231 for k, v in config_override.items():
3232 options.append('--%s=%s' % (k, v))
3234 if out_discard:
3235 out_filename = '/dev/null'
3237 out_mustclose = False
3238 if out_filename is not None:
3239 out_mustclose = True
3240 out_stream = open(out_filename, 'wb')
3242 if in_filename is not None:
3243 in_stream = open(in_filename, 'rb')
3245 if in_string is not None:
3246 in_stream = BytesIO(in_string)
3248 encoding_gmt = gmt_config.get(
3249 'PS_CHAR_ENCODING',
3250 gmt_config.get('CHAR_ENCODING', 'ISOLatin1+'))
3252 encoding = encoding_gmt_to_python[encoding_gmt.lower()]
3254 if in_columns is not None or in_rows is not None:
3255 in_stream = LineStreamChopper(TableLiner(in_columns=in_columns,
3256 in_rows=in_rows,
3257 encoding=encoding))
3259 # convert option arguments to strings
3260 for k, v in kwargs.items():
3261 if len(k) > 1:
3262 raise GmtPyError('Found illegal keyword argument "%s" '
3263 'while preparing options for command "%s"'
3264 % (k, command))
3266 if type(v) is bool:
3267 if v:
3268 options.append('-%s' % k)
3269 elif type(v) is tuple or type(v) is list:
3270 options.append('-%s' % k + '/'.join([str(x) for x in v]))
3271 else:
3272 options.append('-%s%s' % (k, str(v)))
3274 # if not redirecting to an external sink, handle -K -O
3275 if out_stream is None:
3276 if not finish:
3277 options.append('-K')
3278 else:
3279 self.finished = True
3281 if not self.needstart:
3282 options.append('-O')
3283 else:
3284 self.needstart = False
3286 out_stream = self.output
3288 # run the command
3289 if self.is_gmt5():
3290 args = [pjoin(self.installation['bin'], 'gmt'), command]
3291 else:
3292 args = [pjoin(self.installation['bin'], command)]
3294 if not os.path.isfile(args[0]):
3295 raise OSError('No such file: %s' % args[0])
3296 args.extend(options)
3297 args.extend(addargs)
3298 if not self.is_gmt5() and not suppressdefaults:
3299 # does not seem to work with GMT 5 (and should not be necessary
3300 args.append('+'+gmt_config_filename)
3302 bs = 2048
3303 p = subprocess.Popen(args, stdin=subprocess.PIPE,
3304 stdout=subprocess.PIPE, bufsize=bs,
3305 env=self.environ)
3306 while True:
3307 cr, cw, cx = select([p.stdout], [p.stdin], [])
3308 if cr:
3309 out_stream.write(p.stdout.read(bs))
3310 if cw:
3311 if in_stream is not None:
3312 data = in_stream.read(bs)
3313 if len(data) == 0:
3314 break
3315 p.stdin.write(data)
3316 else:
3317 break
3318 if not cr and not cw:
3319 break
3321 p.stdin.close()
3323 while True:
3324 data = p.stdout.read(bs)
3325 if len(data) == 0:
3326 break
3327 out_stream.write(data)
3329 p.stdout.close()
3331 retcode = p.wait()
3333 if in_stream is not None:
3334 in_stream.close()
3336 if out_mustclose:
3337 out_stream.close()
3339 if retcode != 0:
3340 self.keep_temp_dir = True
3341 raise GMTError('Command %s returned an error. '
3342 'While executing command:\n%s'
3343 % (command, escape_shell_args(args)))
3345 self.command_log.append(args)
3347 def __getattr__(self, command):
3349 '''
3350 Maps to call self._gmtcommand(command, \\*addargs, \\*\\*kwargs).
3352 Execute arbitrary GMT command.
3354 Run a GMT command and by default append its postscript output to the
3355 output file maintained by the GMT instance on which this method is
3356 called.
3358 Except for a few keyword arguments listed below, any ``kwargs`` and
3359 ``addargs`` are converted into command line options and arguments and
3360 passed to the GMT command. Numbers in keyword arguments are converted
3361 into strings. E.g. ``S=10`` is translated into ``'-S10'``. Tuples of
3362 numbers or strings are converted into strings where the elements of the
3363 tuples are separated by slashes '/'. E.g. ``R=(10, 10, 20, 20)`` is
3364 translated into ``'-R10/10/20/20'``. Options with a boolean argument
3365 are only appended to the GMT command, if their values are True.
3367 If no output redirection is in effect, the -K and -O options are
3368 handled by gmtpy and thus should not be specified. Use
3369 ``out_discard=True`` if you don't want -K or -O beeing added, but are
3370 not interested in the output.
3372 The standard input of the GMT process is fed by data selected with one
3373 of the following ``in_*`` keyword arguments:
3375 =============== =======================================================
3376 ``in_stream`` Data is read from an open file like object.
3377 ``in_filename`` Data is read from the given file.
3378 ``in_string`` String content is dumped to the process.
3379 ``in_columns`` A 2D nested iterable whose elements can be accessed as
3380 ``in_columns[icolumn][irow]`` is converted into an
3381 ascii
3382 table, which is fed to the process.
3383 ``in_rows`` A 2D nested iterable whos elements can be accessed as
3384 ``in_rows[irow][icolumn]`` is converted into an ascii
3385 table, which is fed to the process.
3386 =============== =======================================================
3388 The standard output of the GMT process may be redirected by one of the
3389 following options:
3391 ================= =====================================================
3392 ``out_stream`` Output is fed to an open file like object.
3393 ``out_filename`` Output is dumped to the given file.
3394 ``out_discard`` If True, output is dumped to :file:`/dev/null`.
3395 ================= =====================================================
3397 Additional keyword arguments:
3399 ===================== =================================================
3400 ``config`` Dict with GMT defaults which override the
3401 currently active set of defaults exclusively
3402 during this call.
3403 ``finish`` If True, the postscript file, which is maintained
3404 by the GMT instance is finished, and no further
3405 plotting is allowed.
3406 ``suppress_defaults`` Suppress appending of the ``'+gmtdefaults'``
3407 option to the command.
3408 ===================== =================================================
3410 '''
3412 def f(*args, **kwargs):
3413 return self._gmtcommand(command, *args, **kwargs)
3414 return f
3416 def tempfilename(self, name=None):
3417 '''
3418 Get filename for temporary file in the private temp directory.
3420 If no ``name`` argument is given, a random name is picked. If
3421 ``name`` is given, returns a path ending in that ``name``.
3422 '''
3424 if not name:
3425 name = ''.join(
3426 [random.choice('abcdefghijklmnopqrstuvwxyz')
3427 for i in range(10)])
3429 fn = pjoin(self.tempdir, name)
3430 return fn
3432 def tempfile(self, name=None):
3433 '''
3434 Create and open a file in the private temp directory.
3435 '''
3437 fn = self.tempfilename(name)
3438 f = open(fn, 'wb')
3439 return f, fn
3441 def save_unfinished(self, filename):
3442 out = open(filename, 'wb')
3443 out.write(self.output.getvalue())
3444 out.close()
3446 def load_unfinished(self, filename):
3447 self.output = BytesIO()
3448 self.finished = False
3449 inp = open(filename, 'rb')
3450 self.output.write(inp.read())
3451 inp.close()
3453 def dump(self, ident):
3454 filename = self.tempfilename('breakpoint-%s' % ident)
3455 self.save_unfinished(filename)
3457 def load(self, ident):
3458 filename = self.tempfilename('breakpoint-%s' % ident)
3459 self.load_unfinished(filename)
3461 def save(self, filename=None, bbox=None, resolution=150, oversample=2.,
3462 width=None, height=None, size=None, crop_eps_mode=False,
3463 psconvert=False):
3465 '''
3466 Finish and save figure as PDF, PS or PPM file.
3468 If filename ends with ``'.pdf'`` a PDF file is created by piping the
3469 GMT output through :program:`gmtpy-epstopdf`.
3471 If filename ends with ``'.png'`` a PNG file is created by running
3472 :program:`gmtpy-epstopdf`, :program:`pdftocairo` and
3473 :program:`convert`. ``resolution`` specifies the resolution in DPI for
3474 raster file formats. Rasterization is done at a higher resolution if
3475 ``oversample`` is set to a value higher than one. The output image size
3476 can also be controlled by setting ``width``, ``height`` or ``size``
3477 instead of ``resolution``. When ``size`` is given, the image is scaled
3478 so that ``max(width, height) == size``.
3480 The bounding box is set according to the values given in ``bbox``.
3481 '''
3483 if not self.finished:
3484 self.psxy(R=True, J=True, finish=True)
3486 if filename:
3487 tempfn = pjoin(self.tempdir, 'incomplete')
3488 out = open(tempfn, 'wb')
3489 else:
3490 out = sys.stdout
3492 if bbox and not self.is_gmt5():
3493 out.write(replace_bbox(bbox, self.output.getvalue()))
3494 else:
3495 out.write(self.output.getvalue())
3497 if filename:
3498 out.close()
3500 if filename.endswith('.ps') or (
3501 not self.is_gmt5() and filename.endswith('.eps')):
3503 shutil.move(tempfn, filename)
3504 return
3506 if self.is_gmt5():
3507 if crop_eps_mode:
3508 addarg = ['-A']
3509 else:
3510 addarg = []
3512 subprocess.call(
3513 [pjoin(self.installation['bin'], 'gmt'), 'psconvert',
3514 '-Te', '-F%s' % tempfn, tempfn, ] + addarg)
3516 if bbox:
3517 with open(tempfn + '.eps', 'rb') as fin:
3518 with open(tempfn + '-fixbb.eps', 'wb') as fout:
3519 replace_bbox(bbox, fin, fout)
3521 shutil.move(tempfn + '-fixbb.eps', tempfn + '.eps')
3523 else:
3524 shutil.move(tempfn, tempfn + '.eps')
3526 if filename.endswith('.eps'):
3527 shutil.move(tempfn + '.eps', filename)
3528 return
3530 elif filename.endswith('.pdf'):
3531 if psconvert:
3532 gmt_bin = pjoin(self.installation['bin'], 'gmt')
3533 subprocess.call([gmt_bin, 'psconvert', tempfn + '.eps', '-Tf',
3534 '-F' + filename])
3535 else:
3536 subprocess.call(['gmtpy-epstopdf', '--res=%i' % resolution,
3537 '--outfile=' + filename, tempfn + '.eps'])
3538 else:
3539 subprocess.call([
3540 'gmtpy-epstopdf',
3541 '--res=%i' % (resolution * oversample),
3542 '--outfile=' + tempfn + '.pdf', tempfn + '.eps'])
3544 convert_graph(
3545 tempfn + '.pdf', filename,
3546 resolution=resolution, oversample=oversample,
3547 size=size, width=width, height=height)
3549 def bbox(self):
3550 return get_bbox(self.output.getvalue())
3552 def get_command_log(self):
3553 '''
3554 Get the command log.
3555 '''
3557 return self.command_log
3559 def __str__(self):
3560 s = ''
3561 for com in self.command_log:
3562 s += com[0] + '\n ' + '\n '.join(com[1:]) + '\n\n'
3563 return s
3565 def page_size_points(self):
3566 '''
3567 Try to get paper size of output postscript file in points.
3568 '''
3570 pm = paper_media(self.gmt_config).lower()
3571 if pm.endswith('+') or pm.endswith('-'):
3572 pm = pm[:-1]
3574 orient = page_orientation(self.gmt_config).lower()
3576 if pm in all_paper_sizes():
3578 if orient == 'portrait':
3579 return get_paper_size(pm)
3580 else:
3581 return get_paper_size(pm)[1], get_paper_size(pm)[0]
3583 m = re.match(r'custom_([0-9.]+)([cimp]?)x([0-9.]+)([cimp]?)', pm)
3584 if m:
3585 w, uw, h, uh = m.groups()
3586 w, h = float(w), float(h)
3587 if uw:
3588 w *= _units[uw]
3589 if uh:
3590 h *= _units[uh]
3591 if orient == 'portrait':
3592 return w, h
3593 else:
3594 return h, w
3596 return None, None
3598 def default_layout(self, with_palette=False):
3599 '''
3600 Get a default layout for the output page.
3602 One of three different layouts is choosen, depending on the
3603 `PAPER_MEDIA` setting in the GMT configuration dict.
3605 If `PAPER_MEDIA` ends with a ``'+'`` (EPS output is selected), a
3606 :py:class:`FrameLayout` is centered on the page, whose size is
3607 controlled by its center widget's size plus the margins of the
3608 :py:class:`FrameLayout`.
3610 If `PAPER_MEDIA` indicates, that a custom page size is wanted by
3611 starting with ``'Custom_'``, a :py:class:`FrameLayout` is used to fill
3612 the complete page. The center widget's size is then controlled by the
3613 page's size minus the margins of the :py:class:`FrameLayout`.
3615 In any other case, two FrameLayouts are nested, such that the outer
3616 layout attaches a 1 cm (printer) margin around the complete page, and
3617 the inner FrameLayout's center widget takes up as much space as
3618 possible under the constraint, that an aspect ratio of 1/golden_ratio
3619 is preserved.
3621 In any case, a reference to the innermost :py:class:`FrameLayout`
3622 instance is returned. The top-level layout can be accessed by calling
3623 :py:meth:`Widget.get_parent` on the returned layout.
3624 '''
3626 if self.layout is None:
3627 w, h = self.page_size_points()
3629 if w is None or h is None:
3630 raise GmtPyError("Can't determine page size for layout")
3632 pm = paper_media(self.gmt_config).lower()
3634 if with_palette:
3635 palette_layout = GridLayout(3, 1)
3636 spacer = palette_layout.get_widget(1, 0)
3637 palette_widget = palette_layout.get_widget(2, 0)
3638 spacer.set_horizontal(0.5*cm)
3639 palette_widget.set_horizontal(0.5*cm)
3641 if pm.endswith('+') or self.eps_mode:
3642 outer = CenterLayout()
3643 outer.set_policy((w, h), (0., 0.))
3644 inner = FrameLayout()
3645 outer.set_widget(inner)
3646 if with_palette:
3647 inner.set_widget('center', palette_layout)
3648 widget = palette_layout
3649 else:
3650 widget = inner.get_widget('center')
3651 widget.set_policy((w/golden_ratio, 0.), (0., 0.),
3652 aspect=1./golden_ratio)
3653 mw = 3.0*cm
3654 inner.set_fixed_margins(
3655 mw, mw, mw/golden_ratio, mw/golden_ratio)
3656 self.layout = inner
3658 elif pm.startswith('custom_'):
3659 layout = FrameLayout()
3660 layout.set_policy((w, h), (0., 0.))
3661 mw = 3.0*cm
3662 layout.set_min_margins(
3663 mw, mw, mw/golden_ratio, mw/golden_ratio)
3664 if with_palette:
3665 layout.set_widget('center', palette_layout)
3666 self.layout = layout
3667 else:
3668 outer = FrameLayout()
3669 outer.set_policy((w, h), (0., 0.))
3670 outer.set_fixed_margins(1.*cm, 1.*cm, 1.*cm, 1.*cm)
3672 inner = FrameLayout()
3673 outer.set_widget('center', inner)
3674 mw = 3.0*cm
3675 inner.set_min_margins(mw, mw, mw/golden_ratio, mw/golden_ratio)
3676 if with_palette:
3677 inner.set_widget('center', palette_layout)
3678 widget = palette_layout
3679 else:
3680 widget = inner.get_widget('center')
3682 widget.set_aspect(1./golden_ratio)
3684 self.layout = inner
3686 return self.layout
3688 def draw_layout(self, layout):
3689 '''
3690 Use psxy to draw layout; for debugging
3691 '''
3693 # corners = layout.get_corners(descend=True)
3694 rects = num.array(layout.get_sizes(), dtype=float)
3695 rects_wid = rects[:, 0, 0]
3696 rects_hei = rects[:, 0, 1]
3697 rects_center_x = rects[:, 1, 0] + rects_wid*0.5
3698 rects_center_y = rects[:, 1, 1] + rects_hei*0.5
3699 nrects = len(rects)
3700 prects = (rects_center_x, rects_center_y, num.arange(nrects),
3701 num.zeros(nrects), rects_hei, rects_wid)
3703 # points = num.array(corners, dtype=float)
3705 cptfile = self.tempfilename() + '.cpt'
3706 self.makecpt(
3707 C='ocean',
3708 T='%g/%g/%g' % (-nrects, nrects, 1),
3709 Z=True,
3710 out_filename=cptfile, suppress_defaults=True)
3712 bb = layout.bbox()
3713 self.psxy(
3714 in_columns=prects,
3715 C=cptfile,
3716 W='1p',
3717 S='J',
3718 R=(bb[0], bb[2], bb[1], bb[3]),
3719 *layout.XYJ())
3722def simpleconf_to_ax(conf, axname):
3723 c = {}
3724 x = axname
3725 for x in ('', axname):
3726 for k in ('label', 'unit', 'scaled_unit', 'scaled_unit_factor',
3727 'space', 'mode', 'approx_ticks', 'limits', 'masking', 'inc',
3728 'snap'):
3730 if x+k in conf:
3731 c[k] = conf[x+k]
3733 return Ax(**c)
3736class DensityPlotDef(object):
3737 def __init__(self, data, cpt='ocean', tension=0.7, size=(640, 480),
3738 contour=False, method='surface', zscaler=None, **extra):
3739 self.data = data
3740 self.cpt = cpt
3741 self.tension = tension
3742 self.size = size
3743 self.contour = contour
3744 self.method = method
3745 self.zscaler = zscaler
3746 self.extra = extra
3749class TextDef(object):
3750 def __init__(
3751 self,
3752 data,
3753 size=9,
3754 justify='MC',
3755 fontno=0,
3756 offset=(0, 0),
3757 color='black'):
3759 self.data = data
3760 self.size = size
3761 self.justify = justify
3762 self.fontno = fontno
3763 self.offset = offset
3764 self.color = color
3767class Simple(object):
3768 def __init__(self, gmtconfig=None, gmtversion='newest', **simple_config):
3769 self.data = []
3770 self.symbols = []
3771 self.config = copy.deepcopy(simple_config)
3772 self.gmtconfig = gmtconfig
3773 self.density_plot_defs = []
3774 self.text_defs = []
3776 self.gmtversion = gmtversion
3778 self.data_x = []
3779 self.symbols_x = []
3781 self.data_y = []
3782 self.symbols_y = []
3784 self.default_config = {}
3785 self.set_defaults(width=15.*cm,
3786 height=15.*cm / golden_ratio,
3787 margins=(2.*cm, 2.*cm, 2.*cm, 2.*cm),
3788 with_palette=False,
3789 palette_offset=0.5*cm,
3790 palette_width=None,
3791 palette_height=None,
3792 zlabeloffset=2*cm,
3793 draw_layout=False)
3795 self.setup_defaults()
3796 self.fixate_widget_aspect = False
3798 def setup_defaults(self):
3799 pass
3801 def set_defaults(self, **kwargs):
3802 self.default_config.update(kwargs)
3804 def plot(self, data, symbol=''):
3805 self.data.append(data)
3806 self.symbols.append(symbol)
3808 def density_plot(self, data, **kwargs):
3809 dpd = DensityPlotDef(data, **kwargs)
3810 self.density_plot_defs.append(dpd)
3812 def text(self, data, **kwargs):
3813 dpd = TextDef(data, **kwargs)
3814 self.text_defs.append(dpd)
3816 def plot_x(self, data, symbol=''):
3817 self.data_x.append(data)
3818 self.symbols_x.append(symbol)
3820 def plot_y(self, data, symbol=''):
3821 self.data_y.append(data)
3822 self.symbols_y.append(symbol)
3824 def set(self, **kwargs):
3825 self.config.update(kwargs)
3827 def setup_base(self, conf):
3828 w = conf.pop('width')
3829 h = conf.pop('height')
3830 margins = conf.pop('margins')
3832 gmtconfig = {}
3833 if self.gmtconfig is not None:
3834 gmtconfig.update(self.gmtconfig)
3836 gmt = GMT(
3837 version=self.gmtversion,
3838 config=gmtconfig,
3839 config_papersize='Custom_%ix%i' % (w, h))
3841 layout = gmt.default_layout(with_palette=conf['with_palette'])
3842 layout.set_min_margins(*margins)
3843 if conf['with_palette']:
3844 widget = layout.get_widget().get_widget(0, 0)
3845 spacer = layout.get_widget().get_widget(1, 0)
3846 spacer.set_horizontal(conf['palette_offset'])
3847 palette_widget = layout.get_widget().get_widget(2, 0)
3848 if conf['palette_width'] is not None:
3849 palette_widget.set_horizontal(conf['palette_width'])
3850 if conf['palette_height'] is not None:
3851 palette_widget.set_vertical(conf['palette_height'])
3852 widget.set_vertical(h-margins[2]-margins[3]-0.03*cm)
3853 return gmt, layout, widget, palette_widget
3854 else:
3855 widget = layout.get_widget()
3856 return gmt, layout, widget, None
3858 def setup_projection(self, widget, scaler, conf):
3859 pass
3861 def setup_scaling(self, conf):
3862 ndims = 2
3863 if self.density_plot_defs:
3864 ndims = 3
3866 axes = [simpleconf_to_ax(conf, x) for x in 'xyz'[:ndims]]
3868 data_all = []
3869 data_all.extend(self.data)
3870 for dsd in self.density_plot_defs:
3871 if dsd.zscaler is None:
3872 data_all.append(dsd.data)
3873 else:
3874 data_all.append(dsd.data[:2])
3875 data_chopped = [ds[:ndims] for ds in data_all]
3877 scaler = ScaleGuru(data_chopped, axes=axes[:ndims])
3879 self.setup_scaling_plus(scaler, axes[:ndims])
3881 return scaler
3883 def setup_scaling_plus(self, scaler, axes):
3884 pass
3886 def setup_scaling_extra(self, scaler, conf):
3888 scaler_x = scaler.copy()
3889 scaler_x.data_ranges[1] = (0., 1.)
3890 scaler_x.axes[1].mode = 'off'
3892 scaler_y = scaler.copy()
3893 scaler_y.data_ranges[0] = (0., 1.)
3894 scaler_y.axes[0].mode = 'off'
3896 return scaler_x, scaler_y
3898 def draw_density(self, gmt, widget, scaler):
3900 R = scaler.R()
3901 # par = scaler.get_params()
3902 rxyj = R + widget.XYJ()
3903 innerticks = False
3904 for dpd in self.density_plot_defs:
3906 fn_cpt = gmt.tempfilename() + '.cpt'
3908 if dpd.zscaler is not None:
3909 s = dpd.zscaler
3910 else:
3911 s = scaler
3913 gmt.makecpt(C=dpd.cpt, out_filename=fn_cpt, *s.T())
3915 fn_grid = gmt.tempfilename()
3917 fn_mean = gmt.tempfilename()
3919 if dpd.method in ('surface', 'triangulate'):
3920 gmt.blockmean(in_columns=dpd.data,
3921 I='%i+/%i+' % dpd.size, # noqa
3922 out_filename=fn_mean, *R)
3924 if dpd.method == 'surface':
3925 gmt.surface(
3926 in_filename=fn_mean,
3927 T=dpd.tension,
3928 G=fn_grid,
3929 I='%i+/%i+' % dpd.size, # noqa
3930 out_discard=True,
3931 *R)
3933 if dpd.method == 'triangulate':
3934 gmt.triangulate(
3935 in_filename=fn_mean,
3936 G=fn_grid,
3937 I='%i+/%i+' % dpd.size, # noqa
3938 out_discard=True,
3939 V=True,
3940 *R)
3942 if gmt.is_gmt5():
3943 gmt.grdimage(fn_grid, C=fn_cpt, E='i', n='l', *rxyj)
3945 else:
3946 gmt.grdimage(fn_grid, C=fn_cpt, E='i', S='l', *rxyj)
3948 if dpd.contour:
3949 gmt.grdcontour(fn_grid, C=fn_cpt, W='0.5p,black', *rxyj)
3950 innerticks = '0.5p,black'
3952 os.remove(fn_grid)
3953 os.remove(fn_mean)
3955 if dpd.method == 'fillcontour':
3956 extra = dict(C=fn_cpt)
3957 extra.update(dpd.extra)
3958 gmt.pscontour(in_columns=dpd.data,
3959 I=True, *rxyj, **extra) # noqa
3961 if dpd.method == 'contour':
3962 extra = dict(W='0.5p,black', C=fn_cpt)
3963 extra.update(dpd.extra)
3964 gmt.pscontour(in_columns=dpd.data, *rxyj, **extra)
3966 return fn_cpt, innerticks
3968 def draw_basemap(self, gmt, widget, scaler):
3969 gmt.psbasemap(*(widget.JXY() + scaler.RB(ax_projection=True)))
3971 def draw(self, gmt, widget, scaler):
3972 rxyj = scaler.R() + widget.JXY()
3973 for dat, sym in zip(self.data, self.symbols):
3974 gmt.psxy(in_columns=dat, *(sym.split()+rxyj))
3976 def post_draw(self, gmt, widget, scaler):
3977 pass
3979 def pre_draw(self, gmt, widget, scaler):
3980 pass
3982 def draw_extra(self, gmt, widget, scaler_x, scaler_y):
3984 for dat, sym in zip(self.data_x, self.symbols_x):
3985 gmt.psxy(in_columns=dat,
3986 *(sym.split() + scaler_x.R() + widget.JXY()))
3988 for dat, sym in zip(self.data_y, self.symbols_y):
3989 gmt.psxy(in_columns=dat,
3990 *(sym.split() + scaler_y.R() + widget.JXY()))
3992 def draw_text(self, gmt, widget, scaler):
3994 rxyj = scaler.R() + widget.JXY()
3995 for td in self.text_defs:
3996 x, y = td.data[0:2]
3997 text = td.data[-1]
3998 size = td.size
3999 angle = 0
4000 fontno = td.fontno
4001 justify = td.justify
4002 color = td.color
4003 if gmt.is_gmt5():
4004 gmt.pstext(
4005 in_rows=[(x, y, text)],
4006 F='+f%gp,%s,%s+a%g+j%s' % (
4007 size, fontno, color, angle, justify),
4008 D='%gp/%gp' % td.offset, *rxyj)
4009 else:
4010 gmt.pstext(
4011 in_rows=[(x, y, size, angle, fontno, justify, text)],
4012 D='%gp/%gp' % td.offset, *rxyj)
4014 def save(self, filename, resolution=150):
4016 conf = dict(self.default_config)
4017 conf.update(self.config)
4019 gmt, layout, widget, palette_widget = self.setup_base(conf)
4020 scaler = self.setup_scaling(conf)
4021 scaler_x, scaler_y = self.setup_scaling_extra(scaler, conf)
4023 self.setup_projection(widget, scaler, conf)
4024 if self.fixate_widget_aspect:
4025 aspect = aspect_for_projection(
4026 gmt.installation['version'], *(widget.J() + scaler.R()))
4028 widget.set_aspect(aspect)
4030 if conf['draw_layout']:
4031 gmt.draw_layout(layout)
4032 cptfile = None
4033 if self.density_plot_defs:
4034 cptfile, innerticks = self.draw_density(gmt, widget, scaler)
4035 self.pre_draw(gmt, widget, scaler)
4036 self.draw(gmt, widget, scaler)
4037 self.post_draw(gmt, widget, scaler)
4038 self.draw_extra(gmt, widget, scaler_x, scaler_y)
4039 self.draw_text(gmt, widget, scaler)
4040 self.draw_basemap(gmt, widget, scaler)
4042 if palette_widget and cptfile:
4043 nice_palette(gmt, palette_widget, scaler, cptfile,
4044 innerticks=innerticks,
4045 zlabeloffset=conf['zlabeloffset'])
4047 gmt.save(filename, resolution=resolution)
4050class LinLinPlot(Simple):
4051 pass
4054class LogLinPlot(Simple):
4056 def setup_defaults(self):
4057 self.set_defaults(xmode='min-max')
4059 def setup_projection(self, widget, scaler, conf):
4060 widget['J'] = '-JX%(width)gpl/%(height)gp'
4061 scaler['B'] = '-B2:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen'
4064class LinLogPlot(Simple):
4066 def setup_defaults(self):
4067 self.set_defaults(ymode='min-max')
4069 def setup_projection(self, widget, scaler, conf):
4070 widget['J'] = '-JX%(width)gp/%(height)gpl'
4071 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/2:%(ylabel)s:WSen'
4074class LogLogPlot(Simple):
4076 def setup_defaults(self):
4077 self.set_defaults(mode='min-max')
4079 def setup_projection(self, widget, scaler, conf):
4080 widget['J'] = '-JX%(width)gpl/%(height)gpl'
4081 scaler['B'] = '-B2:%(xlabel)s:/2:%(ylabel)s:WSen'
4084class AziDistPlot(Simple):
4086 def __init__(self, *args, **kwargs):
4087 Simple.__init__(self, *args, **kwargs)
4088 self.fixate_widget_aspect = True
4090 def setup_defaults(self):
4091 self.set_defaults(
4092 height=15.*cm,
4093 width=15.*cm,
4094 xmode='off',
4095 xlimits=(0., 360.),
4096 xinc=45.)
4098 def setup_projection(self, widget, scaler, conf):
4099 widget['J'] = '-JPa%(width)gp'
4101 def setup_scaling_plus(self, scaler, axes):
4102 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/%(yinc)g:%(ylabel)s:N'
4105class MPlot(Simple):
4107 def __init__(self, *args, **kwargs):
4108 Simple.__init__(self, *args, **kwargs)
4109 self.fixate_widget_aspect = True
4111 def setup_defaults(self):
4112 self.set_defaults(xmode='min-max', ymode='min-max')
4114 def setup_projection(self, widget, scaler, conf):
4115 par = scaler.get_params()
4116 lon0 = (par['xmin'] + par['xmax'])/2.
4117 lat0 = (par['ymin'] + par['ymax'])/2.
4118 sll = '%g/%g' % (lon0, lat0)
4119 widget['J'] = '-JM' + sll + '/%(width)gp'
4120 scaler['B'] = \
4121 '-B%(xinc)gg%(xinc)g:%(xlabel)s:/%(yinc)gg%(yinc)g:%(ylabel)s:WSen'
4124def nice_palette(gmt, widget, scaleguru, cptfile, zlabeloffset=0.8*inch,
4125 innerticks=True):
4127 par = scaleguru.get_params()
4128 par_ax = scaleguru.get_params(ax_projection=True)
4129 nz_palette = int(widget.height()/inch * 300)
4130 px = num.zeros(nz_palette*2)
4131 px[1::2] += 1
4132 pz = num.linspace(par['zmin'], par['zmax'], nz_palette).repeat(2)
4133 pdz = pz[2]-pz[0]
4134 palgrdfile = gmt.tempfilename()
4135 pal_r = (0, 1, par['zmin'], par['zmax'])
4136 pal_ax_r = (0, 1, par_ax['zmin'], par_ax['zmax'])
4137 gmt.xyz2grd(
4138 G=palgrdfile, R=pal_r,
4139 I=(1, pdz), in_columns=(px, pz, pz), # noqa
4140 out_discard=True)
4142 gmt.grdimage(palgrdfile, R=pal_r, C=cptfile, *widget.JXY())
4143 if isinstance(innerticks, str):
4144 tickpen = innerticks
4145 gmt.grdcontour(palgrdfile, W=tickpen, R=pal_r, C=cptfile,
4146 *widget.JXY())
4148 negpalwid = '%gp' % -widget.width()
4149 if not isinstance(innerticks, str) and innerticks:
4150 ticklen = negpalwid
4151 else:
4152 ticklen = '0p'
4154 TICK_LENGTH_PARAM = 'MAP_TICK_LENGTH' if gmt.is_gmt5() else 'TICK_LENGTH'
4155 gmt.psbasemap(
4156 R=pal_ax_r, B='4::/%(zinc)g::nsw' % par_ax,
4157 config={TICK_LENGTH_PARAM: ticklen},
4158 *widget.JXY())
4160 if innerticks:
4161 gmt.psbasemap(
4162 R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax,
4163 config={TICK_LENGTH_PARAM: '0p'},
4164 *widget.JXY())
4165 else:
4166 gmt.psbasemap(R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax, *widget.JXY())
4168 if par_ax['zlabel']:
4169 label_font = gmt.label_font()
4170 label_font_size = gmt.label_font_size()
4171 label_offset = zlabeloffset
4172 gmt.pstext(
4173 R=(0, 1, 0, 2), D='%gp/0p' % label_offset,
4174 N=True,
4175 in_rows=[(1, 1, label_font_size, -90, label_font, 'CB',
4176 par_ax['zlabel'])],
4177 *widget.JXY())