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 is 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']
232cm = _units['c']
234# some awsome colors
235tango_colors = {
236 'butter1': (252, 233, 79),
237 'butter2': (237, 212, 0),
238 'butter3': (196, 160, 0),
239 'chameleon1': (138, 226, 52),
240 'chameleon2': (115, 210, 22),
241 'chameleon3': (78, 154, 6),
242 'orange1': (252, 175, 62),
243 'orange2': (245, 121, 0),
244 'orange3': (206, 92, 0),
245 'skyblue1': (114, 159, 207),
246 'skyblue2': (52, 101, 164),
247 'skyblue3': (32, 74, 135),
248 'plum1': (173, 127, 168),
249 'plum2': (117, 80, 123),
250 'plum3': (92, 53, 102),
251 'chocolate1': (233, 185, 110),
252 'chocolate2': (193, 125, 17),
253 'chocolate3': (143, 89, 2),
254 'scarletred1': (239, 41, 41),
255 'scarletred2': (204, 0, 0),
256 'scarletred3': (164, 0, 0),
257 'aluminium1': (238, 238, 236),
258 'aluminium2': (211, 215, 207),
259 'aluminium3': (186, 189, 182),
260 'aluminium4': (136, 138, 133),
261 'aluminium5': (85, 87, 83),
262 'aluminium6': (46, 52, 54)
263}
265graph_colors = [tango_colors[_x] for _x in (
266 'scarletred2', 'skyblue3', 'chameleon3', 'orange2', 'plum2', 'chocolate2',
267 'butter2')]
270def color(x=None):
271 '''
272 Generate a string for GMT option arguments expecting a color.
274 If ``x`` is None, a random color is returned. If it is an integer, the
275 corresponding ``gmtpy.graph_colors[x]`` or black returned. If it is a
276 string and the corresponding ``gmtpy.tango_colors[x]`` exists, this is
277 returned, or the string is passed through. If ``x`` is a tuple, it is
278 transformed into the string form which GMT expects.
279 '''
281 if x is None:
282 return '%i/%i/%i' % tuple(random.randint(0, 255) for _ in 'rgb')
284 if isinstance(x, int):
285 if 0 <= x < len(graph_colors):
286 return '%i/%i/%i' % graph_colors[x]
287 else:
288 return '0/0/0'
290 elif isinstance(x, str):
291 if x in tango_colors:
292 return '%i/%i/%i' % tango_colors[x]
293 else:
294 return x
296 return '%i/%i/%i' % x
299def color_tup(x=None):
300 if x is None:
301 return tuple([random.randint(0, 255) for _x in 'rgb'])
303 if isinstance(x, int):
304 if 0 <= x < len(graph_colors):
305 return graph_colors[x]
306 else:
307 return (0, 0, 0)
309 elif isinstance(x, str):
310 if x in tango_colors:
311 return tango_colors[x]
313 return x
316_gmt_installations = {}
318# Set fixed installation(s) to use...
319# (use this, if you want to use different GMT versions simultaneously.)
321# _gmt_installations['4.2.1'] = {'home': '/sw/etch-ia32/gmt-4.2.1',
322# 'bin': '/sw/etch-ia32/gmt-4.2.1/bin'}
323# _gmt_installations['4.3.0'] = {'home': '/sw/etch-ia32/gmt-4.3.0',
324# 'bin': '/sw/etch-ia32/gmt-4.3.0/bin'}
325# _gmt_installations['6.0.0'] = {'home': '/usr/share/gmt',
326# 'bin': '/usr/bin' }
328# ... or let GmtPy autodetect GMT via $PATH and $GMTHOME
331def key_version(a):
332 a = a.split('_')[0] # get rid of revision id
333 return [int(x) for x in a.split('.')]
336def newest_installed_gmt_version():
337 return sorted(_gmt_installations.keys(), key=key_version)[-1]
340def all_installed_gmt_versions():
341 return sorted(_gmt_installations.keys(), key=key_version)
344# To have consistent defaults, they are hardcoded here and should not be
345# changed.
347_gmt_defaults_by_version = {}
348_gmt_defaults_by_version['4.2.1'] = r'''
349#
350# GMT-SYSTEM 4.2.1 Defaults file
351#
352#-------- Plot Media Parameters -------------
353PAGE_COLOR = 255/255/255
354PAGE_ORIENTATION = portrait
355PAPER_MEDIA = a4+
356#-------- Basemap Annotation Parameters ------
357ANNOT_MIN_ANGLE = 20
358ANNOT_MIN_SPACING = 0
359ANNOT_FONT_PRIMARY = Helvetica
360ANNOT_FONT_SIZE = 12p
361ANNOT_OFFSET_PRIMARY = 0.075i
362ANNOT_FONT_SECONDARY = Helvetica
363ANNOT_FONT_SIZE_SECONDARY = 16p
364ANNOT_OFFSET_SECONDARY = 0.075i
365DEGREE_SYMBOL = ring
366HEADER_FONT = Helvetica
367HEADER_FONT_SIZE = 36p
368HEADER_OFFSET = 0.1875i
369LABEL_FONT = Helvetica
370LABEL_FONT_SIZE = 14p
371LABEL_OFFSET = 0.1125i
372OBLIQUE_ANNOTATION = 1
373PLOT_CLOCK_FORMAT = hh:mm:ss
374PLOT_DATE_FORMAT = yyyy-mm-dd
375PLOT_DEGREE_FORMAT = +ddd:mm:ss
376Y_AXIS_TYPE = hor_text
377#-------- Basemap Layout Parameters ---------
378BASEMAP_AXES = WESN
379BASEMAP_FRAME_RGB = 0/0/0
380BASEMAP_TYPE = plain
381FRAME_PEN = 1.25p
382FRAME_WIDTH = 0.075i
383GRID_CROSS_SIZE_PRIMARY = 0i
384GRID_CROSS_SIZE_SECONDARY = 0i
385GRID_PEN_PRIMARY = 0.25p
386GRID_PEN_SECONDARY = 0.5p
387MAP_SCALE_HEIGHT = 0.075i
388TICK_LENGTH = 0.075i
389POLAR_CAP = 85/90
390TICK_PEN = 0.5p
391X_AXIS_LENGTH = 9i
392Y_AXIS_LENGTH = 6i
393X_ORIGIN = 1i
394Y_ORIGIN = 1i
395UNIX_TIME = FALSE
396UNIX_TIME_POS = -0.75i/-0.75i
397#-------- Color System Parameters -----------
398COLOR_BACKGROUND = 0/0/0
399COLOR_FOREGROUND = 255/255/255
400COLOR_NAN = 128/128/128
401COLOR_IMAGE = adobe
402COLOR_MODEL = rgb
403HSV_MIN_SATURATION = 1
404HSV_MAX_SATURATION = 0.1
405HSV_MIN_VALUE = 0.3
406HSV_MAX_VALUE = 1
407#-------- PostScript Parameters -------------
408CHAR_ENCODING = ISOLatin1+
409DOTS_PR_INCH = 300
410N_COPIES = 1
411PS_COLOR = rgb
412PS_IMAGE_COMPRESS = none
413PS_IMAGE_FORMAT = ascii
414PS_LINE_CAP = round
415PS_LINE_JOIN = miter
416PS_MITER_LIMIT = 35
417PS_VERBOSE = FALSE
418GLOBAL_X_SCALE = 1
419GLOBAL_Y_SCALE = 1
420#-------- I/O Format Parameters -------------
421D_FORMAT = %lg
422FIELD_DELIMITER = tab
423GRIDFILE_SHORTHAND = FALSE
424GRID_FORMAT = nf
425INPUT_CLOCK_FORMAT = hh:mm:ss
426INPUT_DATE_FORMAT = yyyy-mm-dd
427IO_HEADER = FALSE
428N_HEADER_RECS = 1
429OUTPUT_CLOCK_FORMAT = hh:mm:ss
430OUTPUT_DATE_FORMAT = yyyy-mm-dd
431OUTPUT_DEGREE_FORMAT = +D
432XY_TOGGLE = FALSE
433#-------- Projection Parameters -------------
434ELLIPSOID = WGS-84
435MAP_SCALE_FACTOR = default
436MEASURE_UNIT = inch
437#-------- Calendar/Time Parameters ----------
438TIME_FORMAT_PRIMARY = full
439TIME_FORMAT_SECONDARY = full
440TIME_EPOCH = 2000-01-01T00:00:00
441TIME_IS_INTERVAL = OFF
442TIME_INTERVAL_FRACTION = 0.5
443TIME_LANGUAGE = us
444TIME_SYSTEM = other
445TIME_UNIT = d
446TIME_WEEK_START = Sunday
447Y2K_OFFSET_YEAR = 1950
448#-------- Miscellaneous Parameters ----------
449HISTORY = TRUE
450INTERPOLANT = akima
451LINE_STEP = 0.01i
452VECTOR_SHAPE = 0
453VERBOSE = FALSE'''
455_gmt_defaults_by_version['4.3.0'] = r'''
456#
457# GMT-SYSTEM 4.3.0 Defaults file
458#
459#-------- Plot Media Parameters -------------
460PAGE_COLOR = 255/255/255
461PAGE_ORIENTATION = portrait
462PAPER_MEDIA = a4+
463#-------- Basemap Annotation Parameters ------
464ANNOT_MIN_ANGLE = 20
465ANNOT_MIN_SPACING = 0
466ANNOT_FONT_PRIMARY = Helvetica
467ANNOT_FONT_SIZE_PRIMARY = 12p
468ANNOT_OFFSET_PRIMARY = 0.075i
469ANNOT_FONT_SECONDARY = Helvetica
470ANNOT_FONT_SIZE_SECONDARY = 16p
471ANNOT_OFFSET_SECONDARY = 0.075i
472DEGREE_SYMBOL = ring
473HEADER_FONT = Helvetica
474HEADER_FONT_SIZE = 36p
475HEADER_OFFSET = 0.1875i
476LABEL_FONT = Helvetica
477LABEL_FONT_SIZE = 14p
478LABEL_OFFSET = 0.1125i
479OBLIQUE_ANNOTATION = 1
480PLOT_CLOCK_FORMAT = hh:mm:ss
481PLOT_DATE_FORMAT = yyyy-mm-dd
482PLOT_DEGREE_FORMAT = +ddd:mm:ss
483Y_AXIS_TYPE = hor_text
484#-------- Basemap Layout Parameters ---------
485BASEMAP_AXES = WESN
486BASEMAP_FRAME_RGB = 0/0/0
487BASEMAP_TYPE = plain
488FRAME_PEN = 1.25p
489FRAME_WIDTH = 0.075i
490GRID_CROSS_SIZE_PRIMARY = 0i
491GRID_PEN_PRIMARY = 0.25p
492GRID_CROSS_SIZE_SECONDARY = 0i
493GRID_PEN_SECONDARY = 0.5p
494MAP_SCALE_HEIGHT = 0.075i
495POLAR_CAP = 85/90
496TICK_LENGTH = 0.075i
497TICK_PEN = 0.5p
498X_AXIS_LENGTH = 9i
499Y_AXIS_LENGTH = 6i
500X_ORIGIN = 1i
501Y_ORIGIN = 1i
502UNIX_TIME = FALSE
503UNIX_TIME_POS = BL/-0.75i/-0.75i
504UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
505#-------- Color System Parameters -----------
506COLOR_BACKGROUND = 0/0/0
507COLOR_FOREGROUND = 255/255/255
508COLOR_NAN = 128/128/128
509COLOR_IMAGE = adobe
510COLOR_MODEL = rgb
511HSV_MIN_SATURATION = 1
512HSV_MAX_SATURATION = 0.1
513HSV_MIN_VALUE = 0.3
514HSV_MAX_VALUE = 1
515#-------- PostScript Parameters -------------
516CHAR_ENCODING = ISOLatin1+
517DOTS_PR_INCH = 300
518N_COPIES = 1
519PS_COLOR = rgb
520PS_IMAGE_COMPRESS = none
521PS_IMAGE_FORMAT = ascii
522PS_LINE_CAP = round
523PS_LINE_JOIN = miter
524PS_MITER_LIMIT = 35
525PS_VERBOSE = FALSE
526GLOBAL_X_SCALE = 1
527GLOBAL_Y_SCALE = 1
528#-------- I/O Format Parameters -------------
529D_FORMAT = %lg
530FIELD_DELIMITER = tab
531GRIDFILE_SHORTHAND = FALSE
532GRID_FORMAT = nf
533INPUT_CLOCK_FORMAT = hh:mm:ss
534INPUT_DATE_FORMAT = yyyy-mm-dd
535IO_HEADER = FALSE
536N_HEADER_RECS = 1
537OUTPUT_CLOCK_FORMAT = hh:mm:ss
538OUTPUT_DATE_FORMAT = yyyy-mm-dd
539OUTPUT_DEGREE_FORMAT = +D
540XY_TOGGLE = FALSE
541#-------- Projection Parameters -------------
542ELLIPSOID = WGS-84
543MAP_SCALE_FACTOR = default
544MEASURE_UNIT = inch
545#-------- Calendar/Time Parameters ----------
546TIME_FORMAT_PRIMARY = full
547TIME_FORMAT_SECONDARY = full
548TIME_EPOCH = 2000-01-01T00:00:00
549TIME_IS_INTERVAL = OFF
550TIME_INTERVAL_FRACTION = 0.5
551TIME_LANGUAGE = us
552TIME_UNIT = d
553TIME_WEEK_START = Sunday
554Y2K_OFFSET_YEAR = 1950
555#-------- Miscellaneous Parameters ----------
556HISTORY = TRUE
557INTERPOLANT = akima
558LINE_STEP = 0.01i
559VECTOR_SHAPE = 0
560VERBOSE = FALSE'''
563_gmt_defaults_by_version['4.3.1'] = r'''
564#
565# GMT-SYSTEM 4.3.1 Defaults file
566#
567#-------- Plot Media Parameters -------------
568PAGE_COLOR = 255/255/255
569PAGE_ORIENTATION = portrait
570PAPER_MEDIA = a4+
571#-------- Basemap Annotation Parameters ------
572ANNOT_MIN_ANGLE = 20
573ANNOT_MIN_SPACING = 0
574ANNOT_FONT_PRIMARY = Helvetica
575ANNOT_FONT_SIZE_PRIMARY = 12p
576ANNOT_OFFSET_PRIMARY = 0.075i
577ANNOT_FONT_SECONDARY = Helvetica
578ANNOT_FONT_SIZE_SECONDARY = 16p
579ANNOT_OFFSET_SECONDARY = 0.075i
580DEGREE_SYMBOL = ring
581HEADER_FONT = Helvetica
582HEADER_FONT_SIZE = 36p
583HEADER_OFFSET = 0.1875i
584LABEL_FONT = Helvetica
585LABEL_FONT_SIZE = 14p
586LABEL_OFFSET = 0.1125i
587OBLIQUE_ANNOTATION = 1
588PLOT_CLOCK_FORMAT = hh:mm:ss
589PLOT_DATE_FORMAT = yyyy-mm-dd
590PLOT_DEGREE_FORMAT = +ddd:mm:ss
591Y_AXIS_TYPE = hor_text
592#-------- Basemap Layout Parameters ---------
593BASEMAP_AXES = WESN
594BASEMAP_FRAME_RGB = 0/0/0
595BASEMAP_TYPE = plain
596FRAME_PEN = 1.25p
597FRAME_WIDTH = 0.075i
598GRID_CROSS_SIZE_PRIMARY = 0i
599GRID_PEN_PRIMARY = 0.25p
600GRID_CROSS_SIZE_SECONDARY = 0i
601GRID_PEN_SECONDARY = 0.5p
602MAP_SCALE_HEIGHT = 0.075i
603POLAR_CAP = 85/90
604TICK_LENGTH = 0.075i
605TICK_PEN = 0.5p
606X_AXIS_LENGTH = 9i
607Y_AXIS_LENGTH = 6i
608X_ORIGIN = 1i
609Y_ORIGIN = 1i
610UNIX_TIME = FALSE
611UNIX_TIME_POS = BL/-0.75i/-0.75i
612UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
613#-------- Color System Parameters -----------
614COLOR_BACKGROUND = 0/0/0
615COLOR_FOREGROUND = 255/255/255
616COLOR_NAN = 128/128/128
617COLOR_IMAGE = adobe
618COLOR_MODEL = rgb
619HSV_MIN_SATURATION = 1
620HSV_MAX_SATURATION = 0.1
621HSV_MIN_VALUE = 0.3
622HSV_MAX_VALUE = 1
623#-------- PostScript Parameters -------------
624CHAR_ENCODING = ISOLatin1+
625DOTS_PR_INCH = 300
626N_COPIES = 1
627PS_COLOR = rgb
628PS_IMAGE_COMPRESS = none
629PS_IMAGE_FORMAT = ascii
630PS_LINE_CAP = round
631PS_LINE_JOIN = miter
632PS_MITER_LIMIT = 35
633PS_VERBOSE = FALSE
634GLOBAL_X_SCALE = 1
635GLOBAL_Y_SCALE = 1
636#-------- I/O Format Parameters -------------
637D_FORMAT = %lg
638FIELD_DELIMITER = tab
639GRIDFILE_SHORTHAND = FALSE
640GRID_FORMAT = nf
641INPUT_CLOCK_FORMAT = hh:mm:ss
642INPUT_DATE_FORMAT = yyyy-mm-dd
643IO_HEADER = FALSE
644N_HEADER_RECS = 1
645OUTPUT_CLOCK_FORMAT = hh:mm:ss
646OUTPUT_DATE_FORMAT = yyyy-mm-dd
647OUTPUT_DEGREE_FORMAT = +D
648XY_TOGGLE = FALSE
649#-------- Projection Parameters -------------
650ELLIPSOID = WGS-84
651MAP_SCALE_FACTOR = default
652MEASURE_UNIT = inch
653#-------- Calendar/Time Parameters ----------
654TIME_FORMAT_PRIMARY = full
655TIME_FORMAT_SECONDARY = full
656TIME_EPOCH = 2000-01-01T00:00:00
657TIME_IS_INTERVAL = OFF
658TIME_INTERVAL_FRACTION = 0.5
659TIME_LANGUAGE = us
660TIME_UNIT = d
661TIME_WEEK_START = Sunday
662Y2K_OFFSET_YEAR = 1950
663#-------- Miscellaneous Parameters ----------
664HISTORY = TRUE
665INTERPOLANT = akima
666LINE_STEP = 0.01i
667VECTOR_SHAPE = 0
668VERBOSE = FALSE'''
671_gmt_defaults_by_version['4.4.0'] = r'''
672#
673# GMT-SYSTEM 4.4.0 [64-bit] Defaults file
674#
675#-------- Plot Media Parameters -------------
676PAGE_COLOR = 255/255/255
677PAGE_ORIENTATION = portrait
678PAPER_MEDIA = a4+
679#-------- Basemap Annotation Parameters ------
680ANNOT_MIN_ANGLE = 20
681ANNOT_MIN_SPACING = 0
682ANNOT_FONT_PRIMARY = Helvetica
683ANNOT_FONT_SIZE_PRIMARY = 14p
684ANNOT_OFFSET_PRIMARY = 0.075i
685ANNOT_FONT_SECONDARY = Helvetica
686ANNOT_FONT_SIZE_SECONDARY = 16p
687ANNOT_OFFSET_SECONDARY = 0.075i
688DEGREE_SYMBOL = ring
689HEADER_FONT = Helvetica
690HEADER_FONT_SIZE = 36p
691HEADER_OFFSET = 0.1875i
692LABEL_FONT = Helvetica
693LABEL_FONT_SIZE = 14p
694LABEL_OFFSET = 0.1125i
695OBLIQUE_ANNOTATION = 1
696PLOT_CLOCK_FORMAT = hh:mm:ss
697PLOT_DATE_FORMAT = yyyy-mm-dd
698PLOT_DEGREE_FORMAT = +ddd:mm:ss
699Y_AXIS_TYPE = hor_text
700#-------- Basemap Layout Parameters ---------
701BASEMAP_AXES = WESN
702BASEMAP_FRAME_RGB = 0/0/0
703BASEMAP_TYPE = plain
704FRAME_PEN = 1.25p
705FRAME_WIDTH = 0.075i
706GRID_CROSS_SIZE_PRIMARY = 0i
707GRID_PEN_PRIMARY = 0.25p
708GRID_CROSS_SIZE_SECONDARY = 0i
709GRID_PEN_SECONDARY = 0.5p
710MAP_SCALE_HEIGHT = 0.075i
711POLAR_CAP = 85/90
712TICK_LENGTH = 0.075i
713TICK_PEN = 0.5p
714X_AXIS_LENGTH = 9i
715Y_AXIS_LENGTH = 6i
716X_ORIGIN = 1i
717Y_ORIGIN = 1i
718UNIX_TIME = FALSE
719UNIX_TIME_POS = BL/-0.75i/-0.75i
720UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
721#-------- Color System Parameters -----------
722COLOR_BACKGROUND = 0/0/0
723COLOR_FOREGROUND = 255/255/255
724COLOR_NAN = 128/128/128
725COLOR_IMAGE = adobe
726COLOR_MODEL = rgb
727HSV_MIN_SATURATION = 1
728HSV_MAX_SATURATION = 0.1
729HSV_MIN_VALUE = 0.3
730HSV_MAX_VALUE = 1
731#-------- PostScript Parameters -------------
732CHAR_ENCODING = ISOLatin1+
733DOTS_PR_INCH = 300
734N_COPIES = 1
735PS_COLOR = rgb
736PS_IMAGE_COMPRESS = lzw
737PS_IMAGE_FORMAT = ascii
738PS_LINE_CAP = round
739PS_LINE_JOIN = miter
740PS_MITER_LIMIT = 35
741PS_VERBOSE = FALSE
742GLOBAL_X_SCALE = 1
743GLOBAL_Y_SCALE = 1
744#-------- I/O Format Parameters -------------
745D_FORMAT = %lg
746FIELD_DELIMITER = tab
747GRIDFILE_SHORTHAND = FALSE
748GRID_FORMAT = nf
749INPUT_CLOCK_FORMAT = hh:mm:ss
750INPUT_DATE_FORMAT = yyyy-mm-dd
751IO_HEADER = FALSE
752N_HEADER_RECS = 1
753OUTPUT_CLOCK_FORMAT = hh:mm:ss
754OUTPUT_DATE_FORMAT = yyyy-mm-dd
755OUTPUT_DEGREE_FORMAT = +D
756XY_TOGGLE = FALSE
757#-------- Projection Parameters -------------
758ELLIPSOID = WGS-84
759MAP_SCALE_FACTOR = default
760MEASURE_UNIT = inch
761#-------- Calendar/Time Parameters ----------
762TIME_FORMAT_PRIMARY = full
763TIME_FORMAT_SECONDARY = full
764TIME_EPOCH = 2000-01-01T00:00:00
765TIME_IS_INTERVAL = OFF
766TIME_INTERVAL_FRACTION = 0.5
767TIME_LANGUAGE = us
768TIME_UNIT = d
769TIME_WEEK_START = Sunday
770Y2K_OFFSET_YEAR = 1950
771#-------- Miscellaneous Parameters ----------
772HISTORY = TRUE
773INTERPOLANT = akima
774LINE_STEP = 0.01i
775VECTOR_SHAPE = 0
776VERBOSE = FALSE
777'''
779_gmt_defaults_by_version['4.5.2'] = r'''
780#
781# GMT-SYSTEM 4.5.2 [64-bit] Defaults file
782#
783#-------- Plot Media Parameters -------------
784PAGE_COLOR = white
785PAGE_ORIENTATION = portrait
786PAPER_MEDIA = a4+
787#-------- Basemap Annotation Parameters ------
788ANNOT_MIN_ANGLE = 20
789ANNOT_MIN_SPACING = 0
790ANNOT_FONT_PRIMARY = Helvetica
791ANNOT_FONT_SIZE_PRIMARY = 14p
792ANNOT_OFFSET_PRIMARY = 0.075i
793ANNOT_FONT_SECONDARY = Helvetica
794ANNOT_FONT_SIZE_SECONDARY = 16p
795ANNOT_OFFSET_SECONDARY = 0.075i
796DEGREE_SYMBOL = ring
797HEADER_FONT = Helvetica
798HEADER_FONT_SIZE = 36p
799HEADER_OFFSET = 0.1875i
800LABEL_FONT = Helvetica
801LABEL_FONT_SIZE = 14p
802LABEL_OFFSET = 0.1125i
803OBLIQUE_ANNOTATION = 1
804PLOT_CLOCK_FORMAT = hh:mm:ss
805PLOT_DATE_FORMAT = yyyy-mm-dd
806PLOT_DEGREE_FORMAT = +ddd:mm:ss
807Y_AXIS_TYPE = hor_text
808#-------- Basemap Layout Parameters ---------
809BASEMAP_AXES = WESN
810BASEMAP_FRAME_RGB = black
811BASEMAP_TYPE = plain
812FRAME_PEN = 1.25p
813FRAME_WIDTH = 0.075i
814GRID_CROSS_SIZE_PRIMARY = 0i
815GRID_PEN_PRIMARY = 0.25p
816GRID_CROSS_SIZE_SECONDARY = 0i
817GRID_PEN_SECONDARY = 0.5p
818MAP_SCALE_HEIGHT = 0.075i
819POLAR_CAP = 85/90
820TICK_LENGTH = 0.075i
821TICK_PEN = 0.5p
822X_AXIS_LENGTH = 9i
823Y_AXIS_LENGTH = 6i
824X_ORIGIN = 1i
825Y_ORIGIN = 1i
826UNIX_TIME = FALSE
827UNIX_TIME_POS = BL/-0.75i/-0.75i
828UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
829#-------- Color System Parameters -----------
830COLOR_BACKGROUND = black
831COLOR_FOREGROUND = white
832COLOR_NAN = 128
833COLOR_IMAGE = adobe
834COLOR_MODEL = rgb
835HSV_MIN_SATURATION = 1
836HSV_MAX_SATURATION = 0.1
837HSV_MIN_VALUE = 0.3
838HSV_MAX_VALUE = 1
839#-------- PostScript Parameters -------------
840CHAR_ENCODING = ISOLatin1+
841DOTS_PR_INCH = 300
842GLOBAL_X_SCALE = 1
843GLOBAL_Y_SCALE = 1
844N_COPIES = 1
845PS_COLOR = rgb
846PS_IMAGE_COMPRESS = lzw
847PS_IMAGE_FORMAT = ascii
848PS_LINE_CAP = round
849PS_LINE_JOIN = miter
850PS_MITER_LIMIT = 35
851PS_VERBOSE = FALSE
852TRANSPARENCY = 0
853#-------- I/O Format Parameters -------------
854D_FORMAT = %.12lg
855FIELD_DELIMITER = tab
856GRIDFILE_FORMAT = nf
857GRIDFILE_SHORTHAND = FALSE
858INPUT_CLOCK_FORMAT = hh:mm:ss
859INPUT_DATE_FORMAT = yyyy-mm-dd
860IO_HEADER = FALSE
861N_HEADER_RECS = 1
862NAN_RECORDS = pass
863OUTPUT_CLOCK_FORMAT = hh:mm:ss
864OUTPUT_DATE_FORMAT = yyyy-mm-dd
865OUTPUT_DEGREE_FORMAT = D
866XY_TOGGLE = FALSE
867#-------- Projection Parameters -------------
868ELLIPSOID = WGS-84
869MAP_SCALE_FACTOR = default
870MEASURE_UNIT = inch
871#-------- Calendar/Time Parameters ----------
872TIME_FORMAT_PRIMARY = full
873TIME_FORMAT_SECONDARY = full
874TIME_EPOCH = 2000-01-01T00:00:00
875TIME_IS_INTERVAL = OFF
876TIME_INTERVAL_FRACTION = 0.5
877TIME_LANGUAGE = us
878TIME_UNIT = d
879TIME_WEEK_START = Sunday
880Y2K_OFFSET_YEAR = 1950
881#-------- Miscellaneous Parameters ----------
882HISTORY = TRUE
883INTERPOLANT = akima
884LINE_STEP = 0.01i
885VECTOR_SHAPE = 0
886VERBOSE = FALSE
887'''
889_gmt_defaults_by_version['4.5.3'] = r'''
890#
891# GMT-SYSTEM 4.5.3 (CVS Jun 18 2010 10:56:07) [64-bit] Defaults file
892#
893#-------- Plot Media Parameters -------------
894PAGE_COLOR = white
895PAGE_ORIENTATION = portrait
896PAPER_MEDIA = a4+
897#-------- Basemap Annotation Parameters ------
898ANNOT_MIN_ANGLE = 20
899ANNOT_MIN_SPACING = 0
900ANNOT_FONT_PRIMARY = Helvetica
901ANNOT_FONT_SIZE_PRIMARY = 14p
902ANNOT_OFFSET_PRIMARY = 0.075i
903ANNOT_FONT_SECONDARY = Helvetica
904ANNOT_FONT_SIZE_SECONDARY = 16p
905ANNOT_OFFSET_SECONDARY = 0.075i
906DEGREE_SYMBOL = ring
907HEADER_FONT = Helvetica
908HEADER_FONT_SIZE = 36p
909HEADER_OFFSET = 0.1875i
910LABEL_FONT = Helvetica
911LABEL_FONT_SIZE = 14p
912LABEL_OFFSET = 0.1125i
913OBLIQUE_ANNOTATION = 1
914PLOT_CLOCK_FORMAT = hh:mm:ss
915PLOT_DATE_FORMAT = yyyy-mm-dd
916PLOT_DEGREE_FORMAT = +ddd:mm:ss
917Y_AXIS_TYPE = hor_text
918#-------- Basemap Layout Parameters ---------
919BASEMAP_AXES = WESN
920BASEMAP_FRAME_RGB = black
921BASEMAP_TYPE = plain
922FRAME_PEN = 1.25p
923FRAME_WIDTH = 0.075i
924GRID_CROSS_SIZE_PRIMARY = 0i
925GRID_PEN_PRIMARY = 0.25p
926GRID_CROSS_SIZE_SECONDARY = 0i
927GRID_PEN_SECONDARY = 0.5p
928MAP_SCALE_HEIGHT = 0.075i
929POLAR_CAP = 85/90
930TICK_LENGTH = 0.075i
931TICK_PEN = 0.5p
932X_AXIS_LENGTH = 9i
933Y_AXIS_LENGTH = 6i
934X_ORIGIN = 1i
935Y_ORIGIN = 1i
936UNIX_TIME = FALSE
937UNIX_TIME_POS = BL/-0.75i/-0.75i
938UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
939#-------- Color System Parameters -----------
940COLOR_BACKGROUND = black
941COLOR_FOREGROUND = white
942COLOR_NAN = 128
943COLOR_IMAGE = adobe
944COLOR_MODEL = rgb
945HSV_MIN_SATURATION = 1
946HSV_MAX_SATURATION = 0.1
947HSV_MIN_VALUE = 0.3
948HSV_MAX_VALUE = 1
949#-------- PostScript Parameters -------------
950CHAR_ENCODING = ISOLatin1+
951DOTS_PR_INCH = 300
952GLOBAL_X_SCALE = 1
953GLOBAL_Y_SCALE = 1
954N_COPIES = 1
955PS_COLOR = rgb
956PS_IMAGE_COMPRESS = lzw
957PS_IMAGE_FORMAT = ascii
958PS_LINE_CAP = round
959PS_LINE_JOIN = miter
960PS_MITER_LIMIT = 35
961PS_VERBOSE = FALSE
962TRANSPARENCY = 0
963#-------- I/O Format Parameters -------------
964D_FORMAT = %.12lg
965FIELD_DELIMITER = tab
966GRIDFILE_FORMAT = nf
967GRIDFILE_SHORTHAND = FALSE
968INPUT_CLOCK_FORMAT = hh:mm:ss
969INPUT_DATE_FORMAT = yyyy-mm-dd
970IO_HEADER = FALSE
971N_HEADER_RECS = 1
972NAN_RECORDS = pass
973OUTPUT_CLOCK_FORMAT = hh:mm:ss
974OUTPUT_DATE_FORMAT = yyyy-mm-dd
975OUTPUT_DEGREE_FORMAT = D
976XY_TOGGLE = FALSE
977#-------- Projection Parameters -------------
978ELLIPSOID = WGS-84
979MAP_SCALE_FACTOR = default
980MEASURE_UNIT = inch
981#-------- Calendar/Time Parameters ----------
982TIME_FORMAT_PRIMARY = full
983TIME_FORMAT_SECONDARY = full
984TIME_EPOCH = 2000-01-01T00:00:00
985TIME_IS_INTERVAL = OFF
986TIME_INTERVAL_FRACTION = 0.5
987TIME_LANGUAGE = us
988TIME_UNIT = d
989TIME_WEEK_START = Sunday
990Y2K_OFFSET_YEAR = 1950
991#-------- Miscellaneous Parameters ----------
992HISTORY = TRUE
993INTERPOLANT = akima
994LINE_STEP = 0.01i
995VECTOR_SHAPE = 0
996VERBOSE = FALSE
997'''
999_gmt_defaults_by_version['5.1.2'] = r'''
1000#
1001# GMT 5.1.2 Defaults file
1002# vim:sw=8:ts=8:sts=8
1003# $Revision: 13836 $
1004# $LastChangedDate: 2014-12-20 03:45:42 -1000 (Sat, 20 Dec 2014) $
1005#
1006# COLOR Parameters
1007#
1008COLOR_BACKGROUND = black
1009COLOR_FOREGROUND = white
1010COLOR_NAN = 127.5
1011COLOR_MODEL = none
1012COLOR_HSV_MIN_S = 1
1013COLOR_HSV_MAX_S = 0.1
1014COLOR_HSV_MIN_V = 0.3
1015COLOR_HSV_MAX_V = 1
1016#
1017# DIR Parameters
1018#
1019DIR_DATA =
1020DIR_DCW =
1021DIR_GSHHG =
1022#
1023# FONT Parameters
1024#
1025FONT_ANNOT_PRIMARY = 14p,Helvetica,black
1026FONT_ANNOT_SECONDARY = 16p,Helvetica,black
1027FONT_LABEL = 14p,Helvetica,black
1028FONT_LOGO = 8p,Helvetica,black
1029FONT_TITLE = 24p,Helvetica,black
1030#
1031# FORMAT Parameters
1032#
1033FORMAT_CLOCK_IN = hh:mm:ss
1034FORMAT_CLOCK_OUT = hh:mm:ss
1035FORMAT_CLOCK_MAP = hh:mm:ss
1036FORMAT_DATE_IN = yyyy-mm-dd
1037FORMAT_DATE_OUT = yyyy-mm-dd
1038FORMAT_DATE_MAP = yyyy-mm-dd
1039FORMAT_GEO_OUT = D
1040FORMAT_GEO_MAP = ddd:mm:ss
1041FORMAT_FLOAT_OUT = %.12g
1042FORMAT_FLOAT_MAP = %.12g
1043FORMAT_TIME_PRIMARY_MAP = full
1044FORMAT_TIME_SECONDARY_MAP = full
1045FORMAT_TIME_STAMP = %Y %b %d %H:%M:%S
1046#
1047# GMT Miscellaneous Parameters
1048#
1049GMT_COMPATIBILITY = 4
1050GMT_CUSTOM_LIBS =
1051GMT_EXTRAPOLATE_VAL = NaN
1052GMT_FFT = auto
1053GMT_HISTORY = true
1054GMT_INTERPOLANT = akima
1055GMT_TRIANGULATE = Shewchuk
1056GMT_VERBOSE = compat
1057GMT_LANGUAGE = us
1058#
1059# I/O Parameters
1060#
1061IO_COL_SEPARATOR = tab
1062IO_GRIDFILE_FORMAT = nf
1063IO_GRIDFILE_SHORTHAND = false
1064IO_HEADER = false
1065IO_N_HEADER_RECS = 0
1066IO_NAN_RECORDS = pass
1067IO_NC4_CHUNK_SIZE = auto
1068IO_NC4_DEFLATION_LEVEL = 3
1069IO_LONLAT_TOGGLE = false
1070IO_SEGMENT_MARKER = >
1071#
1072# MAP Parameters
1073#
1074MAP_ANNOT_MIN_ANGLE = 20
1075MAP_ANNOT_MIN_SPACING = 0p
1076MAP_ANNOT_OBLIQUE = 1
1077MAP_ANNOT_OFFSET_PRIMARY = 0.075i
1078MAP_ANNOT_OFFSET_SECONDARY = 0.075i
1079MAP_ANNOT_ORTHO = we
1080MAP_DEFAULT_PEN = default,black
1081MAP_DEGREE_SYMBOL = ring
1082MAP_FRAME_AXES = WESNZ
1083MAP_FRAME_PEN = thicker,black
1084MAP_FRAME_TYPE = fancy
1085MAP_FRAME_WIDTH = 5p
1086MAP_GRID_CROSS_SIZE_PRIMARY = 0p
1087MAP_GRID_CROSS_SIZE_SECONDARY = 0p
1088MAP_GRID_PEN_PRIMARY = default,black
1089MAP_GRID_PEN_SECONDARY = thinner,black
1090MAP_LABEL_OFFSET = 0.1944i
1091MAP_LINE_STEP = 0.75p
1092MAP_LOGO = false
1093MAP_LOGO_POS = BL/-54p/-54p
1094MAP_ORIGIN_X = 1i
1095MAP_ORIGIN_Y = 1i
1096MAP_POLAR_CAP = 85/90
1097MAP_SCALE_HEIGHT = 5p
1098MAP_TICK_LENGTH_PRIMARY = 5p/2.5p
1099MAP_TICK_LENGTH_SECONDARY = 15p/3.75p
1100MAP_TICK_PEN_PRIMARY = thinner,black
1101MAP_TICK_PEN_SECONDARY = thinner,black
1102MAP_TITLE_OFFSET = 14p
1103MAP_VECTOR_SHAPE = 0
1104#
1105# Projection Parameters
1106#
1107PROJ_AUX_LATITUDE = authalic
1108PROJ_ELLIPSOID = WGS-84
1109PROJ_LENGTH_UNIT = cm
1110PROJ_MEAN_RADIUS = authalic
1111PROJ_SCALE_FACTOR = default
1112#
1113# PostScript Parameters
1114#
1115PS_CHAR_ENCODING = ISOLatin1+
1116PS_COLOR_MODEL = rgb
1117PS_COMMENTS = false
1118PS_IMAGE_COMPRESS = deflate,5
1119PS_LINE_CAP = butt
1120PS_LINE_JOIN = miter
1121PS_MITER_LIMIT = 35
1122PS_MEDIA = a4
1123PS_PAGE_COLOR = white
1124PS_PAGE_ORIENTATION = portrait
1125PS_SCALE_X = 1
1126PS_SCALE_Y = 1
1127PS_TRANSPARENCY = Normal
1128#
1129# Calendar/Time Parameters
1130#
1131TIME_EPOCH = 1970-01-01T00:00:00
1132TIME_IS_INTERVAL = off
1133TIME_INTERVAL_FRACTION = 0.5
1134TIME_UNIT = s
1135TIME_WEEK_START = Monday
1136TIME_Y2K_OFFSET_YEAR = 1950
1137'''
1140def get_gmt_version(gmtdefaultsbinary, gmthomedir=None):
1141 args = [gmtdefaultsbinary]
1143 environ = os.environ.copy()
1144 environ['GMTHOME'] = gmthomedir or ''
1146 p = subprocess.Popen(
1147 args,
1148 stdout=subprocess.PIPE,
1149 stderr=subprocess.PIPE,
1150 env=environ)
1152 (stdout, stderr) = p.communicate()
1153 m = re.search(br'(\d+(\.\d+)*)', stderr) \
1154 or re.search(br'# GMT (\d+(\.\d+)*)', stdout)
1156 if not m:
1157 raise GMTInstallationProblem(
1158 "Can't extract version number from output of %s."
1159 % gmtdefaultsbinary)
1161 return str(m.group(1).decode('ascii'))
1164def detect_gmt_installations():
1166 installations = {}
1167 errmesses = []
1169 # GMT 4.x:
1170 try:
1171 p = subprocess.Popen(
1172 ['GMT'],
1173 stdout=subprocess.PIPE,
1174 stderr=subprocess.PIPE)
1176 (stdout, stderr) = p.communicate()
1178 m = re.search(br'Version\s+(\d+(\.\d+)*)', stderr, re.M)
1179 if not m:
1180 raise GMTInstallationProblem(
1181 "Can't get version number from output of GMT.")
1183 version = str(m.group(1).decode('ascii'))
1184 if version[0] != '5':
1186 m = re.search(br'^\s+executables\s+(.+)$', stderr, re.M)
1187 if not m:
1188 raise GMTInstallationProblem(
1189 "Can't extract executables dir from output of GMT.")
1191 gmtbin = str(m.group(1).decode('ascii'))
1193 m = re.search(br'^\s+shared data\s+(.+)$', stderr, re.M)
1194 if not m:
1195 raise GMTInstallationProblem(
1196 "Can't extract shared dir from output of GMT.")
1198 gmtshare = str(m.group(1).decode('ascii'))
1199 if not gmtshare.endswith('/share'):
1200 raise GMTInstallationProblem(
1201 "Can't determine GMTHOME from output of GMT.")
1203 gmthome = gmtshare[:-6]
1205 installations[version] = {
1206 'home': gmthome,
1207 'bin': gmtbin}
1209 except OSError as e:
1210 errmesses.append(('GMT', str(e)))
1212 try:
1213 version = str(subprocess.check_output(
1214 ['gmt', '--version']).strip().decode('ascii')).split('_')[0]
1215 gmtbin = str(subprocess.check_output(
1216 ['gmt', '--show-bindir']).strip().decode('ascii'))
1217 installations[version] = {
1218 'bin': gmtbin}
1220 except (OSError, subprocess.CalledProcessError) as e:
1221 errmesses.append(('gmt', str(e)))
1223 if not installations:
1224 s = []
1225 for (progname, errmess) in errmesses:
1226 s.append('Cannot start "%s" executable: %s' % (progname, errmess))
1228 raise GMTInstallationProblem(', '.join(s))
1230 return installations
1233def appropriate_defaults_version(version):
1234 avails = sorted(_gmt_defaults_by_version.keys(), key=key_version)
1235 for iavail, avail in enumerate(avails):
1236 if key_version(version) == key_version(avail):
1237 return version
1239 elif key_version(version) < key_version(avail):
1240 return avails[max(0, iavail-1)]
1242 return avails[-1]
1245def gmt_default_config(version):
1246 '''
1247 Get default GMT configuration dict for given version.
1248 '''
1250 xversion = appropriate_defaults_version(version)
1252 # if not version in _gmt_defaults_by_version:
1253 # raise GMTError('No GMT defaults for version %s found' % version)
1255 gmt_defaults = _gmt_defaults_by_version[xversion]
1257 d = {}
1258 for line in gmt_defaults.splitlines():
1259 sline = line.strip()
1260 if not sline or sline.startswith('#'):
1261 continue
1263 k, v = sline.split('=', 1)
1264 d[k.strip()] = v.strip()
1266 return d
1269def diff_defaults(v1, v2):
1270 d1 = gmt_default_config(v1)
1271 d2 = gmt_default_config(v2)
1272 for k in d1:
1273 if k not in d2:
1274 print('%s not in %s' % (k, v2))
1275 else:
1276 if d1[k] != d2[k]:
1277 print('%s %s = %s' % (v1, k, d1[k]))
1278 print('%s %s = %s' % (v2, k, d2[k]))
1280 for k in d2:
1281 if k not in d1:
1282 print('%s not in %s' % (k, v1))
1284# diff_defaults('4.5.2', '4.5.3')
1287def check_gmt_installation(installation):
1289 home_dir = installation.get('home', None)
1290 bin_dir = installation['bin']
1291 version = installation['version']
1293 for d in home_dir, bin_dir:
1294 if d is not None:
1295 if not os.path.exists(d):
1296 logging.error(('Directory does not exist: %s\n'
1297 'Check your GMT installation.') % d)
1299 major_version = version.split('.')[0]
1301 if major_version not in ['5', '6']:
1302 gmtdefaults = pjoin(bin_dir, 'gmtdefaults')
1304 versionfound = get_gmt_version(gmtdefaults, home_dir)
1306 if versionfound != version:
1307 raise GMTInstallationProblem((
1308 'Expected GMT version %s but found version %s.\n'
1309 '(Looking at output of %s)') % (
1310 version, versionfound, gmtdefaults))
1313def get_gmt_installation(version):
1314 setup_gmt_installations()
1315 if version != 'newest' and version not in _gmt_installations:
1316 logging.warn('GMT version %s not installed, taking version %s instead'
1317 % (version, newest_installed_gmt_version()))
1319 version = 'newest'
1321 if version == 'newest':
1322 version = newest_installed_gmt_version()
1324 installation = dict(_gmt_installations[version])
1326 return installation
1329def setup_gmt_installations():
1330 if not setup_gmt_installations.have_done:
1331 if not _gmt_installations:
1333 _gmt_installations.update(detect_gmt_installations())
1335 # store defaults as dicts into the gmt installations dicts
1336 for version, installation in _gmt_installations.items():
1337 installation['defaults'] = gmt_default_config(version)
1338 installation['version'] = version
1340 for installation in _gmt_installations.values():
1341 check_gmt_installation(installation)
1343 setup_gmt_installations.have_done = True
1346setup_gmt_installations.have_done = False
1348_paper_sizes_a = '''A0 2380 3368
1349 A1 1684 2380
1350 A2 1190 1684
1351 A3 842 1190
1352 A4 595 842
1353 A5 421 595
1354 A6 297 421
1355 A7 210 297
1356 A8 148 210
1357 A9 105 148
1358 A10 74 105
1359 B0 2836 4008
1360 B1 2004 2836
1361 B2 1418 2004
1362 B3 1002 1418
1363 B4 709 1002
1364 B5 501 709
1365 archA 648 864
1366 archB 864 1296
1367 archC 1296 1728
1368 archD 1728 2592
1369 archE 2592 3456
1370 flsa 612 936
1371 halfletter 396 612
1372 note 540 720
1373 letter 612 792
1374 legal 612 1008
1375 11x17 792 1224
1376 ledger 1224 792'''
1379_paper_sizes = {}
1382def setup_paper_sizes():
1383 if not _paper_sizes:
1384 for line in _paper_sizes_a.splitlines():
1385 k, w, h = line.split()
1386 _paper_sizes[k.lower()] = float(w), float(h)
1389def get_paper_size(k):
1390 setup_paper_sizes()
1391 return _paper_sizes[k.lower().rstrip('+')]
1394def all_paper_sizes():
1395 setup_paper_sizes()
1396 return _paper_sizes
1399def measure_unit(gmt_config):
1400 for k in ['MEASURE_UNIT', 'PROJ_LENGTH_UNIT']:
1401 if k in gmt_config:
1402 return gmt_config[k]
1404 raise GmtPyError('cannot get measure unit / proj length unit from config')
1407def paper_media(gmt_config):
1408 for k in ['PAPER_MEDIA', 'PS_MEDIA']:
1409 if k in gmt_config:
1410 return gmt_config[k]
1412 raise GmtPyError('cannot get paper media from config')
1415def page_orientation(gmt_config):
1416 for k in ['PAGE_ORIENTATION', 'PS_PAGE_ORIENTATION']:
1417 if k in gmt_config:
1418 return gmt_config[k]
1420 raise GmtPyError('cannot get paper orientation from config')
1423def make_bbox(width, height, gmt_config, margins=(0.8, 0.8, 0.8, 0.8)):
1425 leftmargin, topmargin, rightmargin, bottommargin = margins
1426 portrait = page_orientation(gmt_config).lower() == 'portrait'
1428 paper_size = get_paper_size(paper_media(gmt_config))
1429 if not portrait:
1430 paper_size = paper_size[1], paper_size[0]
1432 xoffset = (paper_size[0] - (width + leftmargin + rightmargin)) / \
1433 2.0 + leftmargin
1434 yoffset = (paper_size[1] - (height + topmargin + bottommargin)) / \
1435 2.0 + bottommargin
1437 if portrait:
1438 bb1 = int((xoffset - leftmargin))
1439 bb2 = int((yoffset - bottommargin))
1440 bb3 = bb1 + int((width+leftmargin+rightmargin))
1441 bb4 = bb2 + int((height+topmargin+bottommargin))
1442 else:
1443 bb1 = int((yoffset - topmargin))
1444 bb2 = int((xoffset - leftmargin))
1445 bb3 = bb1 + int((height+topmargin+bottommargin))
1446 bb4 = bb2 + int((width+leftmargin+rightmargin))
1448 return xoffset, yoffset, (bb1, bb2, bb3, bb4)
1451def gmtdefaults_as_text(version='newest'):
1453 '''
1454 Get the built-in gmtdefaults.
1455 '''
1457 if version not in _gmt_installations:
1458 logging.warn('GMT version %s not installed, taking version %s instead'
1459 % (version, newest_installed_gmt_version()))
1460 version = 'newest'
1462 if version == 'newest':
1463 version = newest_installed_gmt_version()
1465 return _gmt_defaults_by_version[version]
1468def savegrd(x, y, z, filename, title=None, naming='xy'):
1469 '''
1470 Write COARDS compliant netcdf (grd) file.
1471 '''
1473 assert y.size, x.size == z.shape
1474 ny, nx = z.shape
1475 nc = netcdf_file(filename, 'w')
1476 assert naming in ('xy', 'lonlat')
1478 if naming == 'xy':
1479 kx, ky = 'x', 'y'
1480 else:
1481 kx, ky = 'lon', 'lat'
1483 nc.node_offset = 0
1484 if title is not None:
1485 nc.title = title
1487 nc.Conventions = 'COARDS/CF-1.0'
1488 nc.createDimension(kx, nx)
1489 nc.createDimension(ky, ny)
1491 xvar = nc.createVariable(kx, 'd', (kx,))
1492 yvar = nc.createVariable(ky, 'd', (ky,))
1493 if naming == 'xy':
1494 xvar.long_name = kx
1495 yvar.long_name = ky
1496 else:
1497 xvar.long_name = 'longitude'
1498 xvar.units = 'degrees_east'
1499 yvar.long_name = 'latitude'
1500 yvar.units = 'degrees_north'
1502 zvar = nc.createVariable('z', 'd', (ky, kx))
1504 xvar[:] = x.astype(num.float64)
1505 yvar[:] = y.astype(num.float64)
1506 zvar[:] = z.astype(num.float64)
1508 nc.close()
1511def to_array(var):
1512 arr = var[:].copy()
1513 if hasattr(var, 'scale_factor'):
1514 arr *= var.scale_factor
1516 if hasattr(var, 'add_offset'):
1517 arr += var.add_offset
1519 return arr
1522def loadgrd(filename):
1523 '''
1524 Read COARDS compliant netcdf (grd) file.
1525 '''
1527 nc = netcdf_file(filename, 'r')
1528 vkeys = list(nc.variables.keys())
1529 kx = 'x'
1530 ky = 'y'
1531 if 'lon' in vkeys:
1532 kx = 'lon'
1533 if 'lat' in vkeys:
1534 ky = 'lat'
1536 kz = 'z'
1537 if 'altitude' in vkeys:
1538 kz = 'altitude'
1540 x = to_array(nc.variables[kx])
1541 y = to_array(nc.variables[ky])
1542 z = to_array(nc.variables[kz])
1544 nc.close()
1545 return x, y, z
1548def centers_to_edges(asorted):
1549 return (asorted[1:] + asorted[:-1])/2.
1552def nvals(asorted):
1553 eps = (asorted[-1]-asorted[0])/asorted.size
1554 return num.sum(asorted[1:] - asorted[:-1] >= eps) + 1
1557def guess_vals(asorted):
1558 eps = (asorted[-1]-asorted[0])/asorted.size
1559 indis = num.nonzero(asorted[1:] - asorted[:-1] >= eps)[0]
1560 indis = num.concatenate((num.array([0]), indis+1,
1561 num.array([asorted.size])))
1562 asum = num.zeros(asorted.size+1)
1563 asum[1:] = num.cumsum(asorted)
1564 return (asum[indis[1:]] - asum[indis[:-1]]) / (indis[1:]-indis[:-1])
1567def blockmean(asorted, b):
1568 indis = num.nonzero(asorted[1:] - asorted[:-1])[0]
1569 indis = num.concatenate((num.array([0]), indis+1,
1570 num.array([asorted.size])))
1571 bsum = num.zeros(b.size+1)
1572 bsum[1:] = num.cumsum(b)
1573 return (
1574 asorted[indis[:-1]],
1575 (bsum[indis[1:]] - bsum[indis[:-1]]) / (indis[1:]-indis[:-1]))
1578def griddata_regular(x, y, z, xvals, yvals):
1579 nx, ny = xvals.size, yvals.size
1580 xindi = num.digitize(x, centers_to_edges(xvals))
1581 yindi = num.digitize(y, centers_to_edges(yvals))
1583 zindi = yindi*nx+xindi
1584 order = num.argsort(zindi)
1585 z = z[order]
1586 zindi = zindi[order]
1588 zindi, z = blockmean(zindi, z)
1589 znew = num.empty(nx*ny, dtype=float)
1590 znew[:] = num.nan
1591 znew[zindi] = z
1592 return znew.reshape(ny, nx)
1595def guess_field_size(x_sorted, y_sorted, z=None, mode=None):
1596 critical_fraction = 1./num.e - 0.014*3
1597 xs = x_sorted
1598 ys = y_sorted
1599 nxs, nys = nvals(xs), nvals(ys)
1600 if mode == 'nonrandom':
1601 return nxs, nys, 0
1602 elif xs.size == nxs*nys:
1603 # exact match
1604 return nxs, nys, 0
1605 elif nxs >= xs.size*critical_fraction and nys >= xs.size*critical_fraction:
1606 # possibly randomly sampled
1607 nxs = int(math.sqrt(xs.size))
1608 nys = nxs
1609 return nxs, nys, 2
1610 else:
1611 return nxs, nys, 1
1614def griddata_auto(x, y, z, mode=None):
1615 '''
1616 Grid tabular XYZ data by binning.
1618 This function does some extra work to guess the size of the grid. This
1619 should work fine if the input values are already defined on an rectilinear
1620 grid, even if data points are missing or duplicated. This routine also
1621 tries to detect a random distribution of input data and in that case
1622 creates a grid of size sqrt(N) x sqrt(N).
1624 The points do not have to be given in any particular order. Grid nodes
1625 without data are assigned the NaN value. If multiple data points map to the
1626 same grid node, their average is assigned to the grid node.
1627 '''
1629 x, y, z = [num.asarray(X) for X in (x, y, z)]
1630 assert x.size == y.size == z.size
1631 xs, ys = num.sort(x), num.sort(y)
1632 nx, ny, badness = guess_field_size(xs, ys, z, mode=mode)
1633 if badness <= 1:
1634 xf = guess_vals(xs)
1635 yf = guess_vals(ys)
1636 zf = griddata_regular(x, y, z, xf, yf)
1637 else:
1638 xf = num.linspace(xs[0], xs[-1], nx)
1639 yf = num.linspace(ys[0], ys[-1], ny)
1640 zf = griddata_regular(x, y, z, xf, yf)
1642 return xf, yf, zf
1645def tabledata(xf, yf, zf):
1646 assert yf.size, xf.size == zf.shape
1647 x = num.tile(xf, yf.size)
1648 y = num.repeat(yf, xf.size)
1649 z = zf.flatten()
1650 return x, y, z
1653def double1d(a):
1654 a2 = num.empty(a.size*2-1)
1655 a2[::2] = a
1656 a2[1::2] = (a[:-1] + a[1:])/2.
1657 return a2
1660def double2d(f):
1661 f2 = num.empty((f.shape[0]*2-1, f.shape[1]*2-1))
1662 f2[:, :] = num.nan
1663 f2[::2, ::2] = f
1664 f2[1::2, ::2] = (f[:-1, :] + f[1:, :])/2.
1665 f2[::2, 1::2] = (f[:, :-1] + f[:, 1:])/2.
1666 f2[1::2, 1::2] = (f[:-1, :-1] + f[1:, :-1] + f[:-1, 1:] + f[1:, 1:])/4.
1667 diag = f2[1::2, 1::2]
1668 diagA = (f[:-1, :-1] + f[1:, 1:]) / 2.
1669 diagB = (f[1:, :-1] + f[:-1, 1:]) / 2.
1670 f2[1::2, 1::2] = num.where(num.isnan(diag), diagA, diag)
1671 f2[1::2, 1::2] = num.where(num.isnan(diag), diagB, diag)
1672 return f2
1675def doublegrid(x, y, z):
1676 x2 = double1d(x)
1677 y2 = double1d(y)
1678 z2 = double2d(z)
1679 return x2, y2, z2
1682class Guru(object):
1683 '''
1684 Abstract base class providing template interpolation, accessible as
1685 attributes.
1687 Classes deriving from this one, have to implement a :py:meth:`get_params`
1688 method, which is called to get a dict to do ordinary
1689 ``"%(key)x"``-substitutions. The deriving class must also provide a dict
1690 with the templates.
1691 '''
1693 def __init__(self):
1694 self.templates = {}
1696 def fill(self, templates, **kwargs):
1697 params = self.get_params(**kwargs)
1698 strings = [t % params for t in templates]
1699 return strings
1701 # hand through templates dict
1702 def __getitem__(self, template_name):
1703 return self.templates[template_name]
1705 def __setitem__(self, template_name, template):
1706 self.templates[template_name] = template
1708 def __contains__(self, template_name):
1709 return template_name in self.templates
1711 def __iter__(self):
1712 return iter(self.templates)
1714 def __len__(self):
1715 return len(self.templates)
1717 def __delitem__(self, template_name):
1718 del self.templates[template_name]
1720 def _simple_fill(self, template_names, **kwargs):
1721 templates = [self.templates[n] for n in template_names]
1722 return self.fill(templates, **kwargs)
1724 def __getattr__(self, template_names):
1725 if [n for n in template_names if n not in self.templates]:
1726 raise AttributeError(template_names)
1728 def f(**kwargs):
1729 return self._simple_fill(template_names, **kwargs)
1731 return f
1734class Ax(AutoScaler):
1735 '''
1736 Ax description with autoscaling capabilities.
1738 The ax is described by the :py:class:`pyrocko.plot.AutoScaler`
1739 public attributes, plus the following additional attributes
1740 (with default values given in paranthesis):
1742 .. py:attribute:: label
1744 Ax label (without unit).
1746 .. py:attribute:: unit
1748 Physical unit of the data attached to this ax.
1750 .. py:attribute:: scaled_unit
1752 (see below)
1754 .. py:attribute:: scaled_unit_factor
1756 Scaled physical unit and factor between unit and scaled_unit so that
1758 unit = scaled_unit_factor x scaled_unit.
1760 (E.g. if unit is 'm' and data is in the range of nanometers, you may
1761 want to set the scaled_unit to 'nm' and the scaled_unit_factor to
1762 1e9.)
1764 .. py:attribute:: limits
1766 If defined, fix range of ax to limits=(min,max).
1768 .. py:attribute:: masking
1770 If true and if there is a limit on the ax, while calculating ranges,
1771 the data points are masked such that data points outside of this axes
1772 limits are not used to determine the range of another dependant ax.
1774 '''
1776 def __init__(self, label='', unit='', scaled_unit_factor=1.,
1777 scaled_unit='', limits=None, masking=True, **kwargs):
1779 AutoScaler.__init__(self, **kwargs)
1780 self.label = label
1781 self.unit = unit
1782 self.scaled_unit_factor = scaled_unit_factor
1783 self.scaled_unit = scaled_unit
1784 self.limits = limits
1785 self.masking = masking
1787 def label_str(self, exp, unit):
1788 '''
1789 Get label string including the unit and multiplier.
1790 '''
1792 slabel, sunit, sexp = '', '', ''
1793 if self.label:
1794 slabel = self.label
1796 if unit or exp != 0:
1797 if exp != 0:
1798 sexp = '\\327 10@+%i@+' % exp
1799 sunit = '[ %s %s ]' % (sexp, unit)
1800 else:
1801 sunit = '[ %s ]' % unit
1803 p = []
1804 if slabel:
1805 p.append(slabel)
1807 if sunit:
1808 p.append(sunit)
1810 return ' '.join(p)
1812 def make_params(self, data_range, ax_projection=False, override_mode=None,
1813 override_scaled_unit_factor=None):
1815 '''
1816 Get minimum, maximum, increment and label string for ax display.'
1818 Returns minimum, maximum, increment and label string including unit and
1819 multiplier for given data range.
1821 If ``ax_projection`` is True, values suitable to be displayed on the ax
1822 are returned, e.g. min, max and inc are returned in scaled units.
1823 Otherwise the values are returned in the original units, without any
1824 scaling applied.
1825 '''
1827 sf = self.scaled_unit_factor
1829 if override_scaled_unit_factor is not None:
1830 sf = override_scaled_unit_factor
1832 dr_scaled = [sf*x for x in data_range]
1834 mi, ma, inc = self.make_scale(dr_scaled, override_mode=override_mode)
1835 if self.inc is not None:
1836 inc = self.inc*sf
1838 if ax_projection:
1839 exp = self.make_exp(inc)
1840 if sf == 1. and override_scaled_unit_factor is None:
1841 unit = self.unit
1842 else:
1843 unit = self.scaled_unit
1844 label = self.label_str(exp, unit)
1845 return mi/10**exp, ma/10**exp, inc/10**exp, label
1846 else:
1847 label = self.label_str(0, self.unit)
1848 return mi/sf, ma/sf, inc/sf, label
1851class ScaleGuru(Guru):
1853 '''
1854 2D/3D autoscaling and ax annotation facility.
1856 Instances of this class provide automatic determination of plot ranges,
1857 tick increments and scaled annotations, as well as label/unit handling. It
1858 can in particular be used to automatically generate the -R and -B option
1859 arguments, which are required for most GMT commands.
1861 It extends the functionality of the :py:class:`Ax` and
1862 :py:class:`AutoScaler` classes at the level, where it can not be handled
1863 anymore by looking at a single dimension of the dataset's data, e.g.:
1865 * The ability to impose a fixed aspect ratio between two axes.
1867 * Recalculation of data range on non-limited axes, when there are
1868 limits imposed on other axes.
1870 '''
1872 def __init__(self, data_tuples=None, axes=None, aspect=None,
1873 percent_interval=None, copy_from=None):
1875 Guru.__init__(self)
1877 if copy_from:
1878 self.templates = copy.deepcopy(copy_from.templates)
1879 self.axes = copy.deepcopy(copy_from.axes)
1880 self.data_ranges = copy.deepcopy(copy_from.data_ranges)
1881 self.aspect = copy_from.aspect
1883 if percent_interval is not None:
1884 from scipy.stats import scoreatpercentile as scap
1886 self.templates = dict(
1887 R='-R%(xmin)g/%(xmax)g/%(ymin)g/%(ymax)g',
1888 B='-B%(xinc)g:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen',
1889 T='-T%(zmin)g/%(zmax)g/%(zinc)g')
1891 maxdim = 2
1892 if data_tuples:
1893 maxdim = max(maxdim, max([len(dt) for dt in data_tuples]))
1894 else:
1895 if axes:
1896 maxdim = len(axes)
1897 data_tuples = [([],) * maxdim]
1898 if axes is not None:
1899 self.axes = axes
1900 else:
1901 self.axes = [Ax() for i in range(maxdim)]
1903 # sophisticated data-range calculation
1904 data_ranges = [None] * maxdim
1905 for dt_ in data_tuples:
1906 dt = num.asarray(dt_)
1907 in_range = True
1908 for ax, x in zip(self.axes, dt):
1909 if ax.limits and ax.masking:
1910 ax_limits = list(ax.limits)
1911 if ax_limits[0] is None:
1912 ax_limits[0] = -num.inf
1913 if ax_limits[1] is None:
1914 ax_limits[1] = num.inf
1915 in_range = num.logical_and(
1916 in_range,
1917 num.logical_and(ax_limits[0] <= x, x <= ax_limits[1]))
1919 for i, ax, x in zip(range(maxdim), self.axes, dt):
1921 if not ax.limits or None in ax.limits:
1922 if len(x) >= 1:
1923 if in_range is not True:
1924 xmasked = num.where(in_range, x, num.NaN)
1925 if percent_interval is None:
1926 range_this = (
1927 num.nanmin(xmasked),
1928 num.nanmax(xmasked))
1929 else:
1930 xmasked_finite = num.compress(
1931 num.isfinite(xmasked), xmasked)
1932 range_this = (
1933 scap(xmasked_finite,
1934 (100.-percent_interval)/2.),
1935 scap(xmasked_finite,
1936 100.-(100.-percent_interval)/2.))
1937 else:
1938 if percent_interval is None:
1939 range_this = num.nanmin(x), num.nanmax(x)
1940 else:
1941 xmasked_finite = num.compress(
1942 num.isfinite(xmasked), xmasked)
1943 range_this = (
1944 scap(xmasked_finite,
1945 (100.-percent_interval)/2.),
1946 scap(xmasked_finite,
1947 100.-(100.-percent_interval)/2.))
1948 else:
1949 range_this = (0., 1.)
1951 if ax.limits:
1952 if ax.limits[0] is not None:
1953 range_this = ax.limits[0], max(ax.limits[0],
1954 range_this[1])
1956 if ax.limits[1] is not None:
1957 range_this = min(ax.limits[1],
1958 range_this[0]), ax.limits[1]
1960 else:
1961 range_this = ax.limits
1963 if data_ranges[i] is None and range_this[0] <= range_this[1]:
1964 data_ranges[i] = range_this
1965 else:
1966 mi, ma = range_this
1967 if data_ranges[i] is not None:
1968 mi = min(data_ranges[i][0], mi)
1969 ma = max(data_ranges[i][1], ma)
1971 data_ranges[i] = (mi, ma)
1973 for i in range(len(data_ranges)):
1974 if data_ranges[i] is None or not (
1975 num.isfinite(data_ranges[i][0])
1976 and num.isfinite(data_ranges[i][1])):
1978 data_ranges[i] = (0., 1.)
1980 self.data_ranges = data_ranges
1981 self.aspect = aspect
1983 def copy(self):
1984 return ScaleGuru(copy_from=self)
1986 def get_params(self, ax_projection=False):
1988 '''
1989 Get dict with output parameters.
1991 For each data dimension, ax minimum, maximum, increment and a label
1992 string (including unit and exponential factor) are determined. E.g. in
1993 for the first dimension the output dict will contain the keys
1994 ``'xmin'``, ``'xmax'``, ``'xinc'``, and ``'xlabel'``.
1996 Normally, values corresponding to the scaling of the raw data are
1997 produced, but if ``ax_projection`` is ``True``, values which are
1998 suitable to be printed on the axes are returned. This means that in the
1999 latter case, the :py:attr:`Ax.scaled_unit` and
2000 :py:attr:`Ax.scaled_unit_factor` attributes as set on the axes are
2001 respected and that a common 10^x factor is factored out and put to the
2002 label string.
2003 '''
2005 xmi, xma, xinc, xlabel = self.axes[0].make_params(
2006 self.data_ranges[0], ax_projection)
2007 ymi, yma, yinc, ylabel = self.axes[1].make_params(
2008 self.data_ranges[1], ax_projection)
2009 if len(self.axes) > 2:
2010 zmi, zma, zinc, zlabel = self.axes[2].make_params(
2011 self.data_ranges[2], ax_projection)
2013 # enforce certain aspect, if needed
2014 if self.aspect is not None:
2015 xwid = xma-xmi
2016 ywid = yma-ymi
2017 if ywid < xwid*self.aspect:
2018 ymi -= (xwid*self.aspect - ywid)*0.5
2019 yma += (xwid*self.aspect - ywid)*0.5
2020 ymi, yma, yinc, ylabel = self.axes[1].make_params(
2021 (ymi, yma), ax_projection, override_mode='off',
2022 override_scaled_unit_factor=1.)
2024 elif xwid < ywid/self.aspect:
2025 xmi -= (ywid/self.aspect - xwid)*0.5
2026 xma += (ywid/self.aspect - xwid)*0.5
2027 xmi, xma, xinc, xlabel = self.axes[0].make_params(
2028 (xmi, xma), ax_projection, override_mode='off',
2029 override_scaled_unit_factor=1.)
2031 params = dict(xmin=xmi, xmax=xma, xinc=xinc, xlabel=xlabel,
2032 ymin=ymi, ymax=yma, yinc=yinc, ylabel=ylabel)
2033 if len(self.axes) > 2:
2034 params.update(dict(zmin=zmi, zmax=zma, zinc=zinc, zlabel=zlabel))
2036 return params
2039class GumSpring(object):
2041 '''
2042 Sizing policy implementing a minimal size, plus a desire to grow.
2043 '''
2045 def __init__(self, minimal=None, grow=None):
2046 self.minimal = minimal
2047 if grow is None:
2048 if minimal is None:
2049 self.grow = 1.0
2050 else:
2051 self.grow = 0.0
2052 else:
2053 self.grow = grow
2054 self.value = 1.0
2056 def get_minimal(self):
2057 if self.minimal is not None:
2058 return self.minimal
2059 else:
2060 return 0.0
2062 def get_grow(self):
2063 return self.grow
2065 def set_value(self, value):
2066 self.value = value
2068 def get_value(self):
2069 return self.value
2072def distribute(sizes, grows, space):
2073 sizes = list(sizes)
2074 gsum = sum(grows)
2075 if gsum > 0.0:
2076 for i in range(len(sizes)):
2077 sizes[i] += space*grows[i]/gsum
2078 return sizes
2081class Widget(Guru):
2083 '''
2084 Base class of the gmtpy layout system.
2086 The Widget class provides the basic functionality for the nesting and
2087 placing of elements on the output page, and maintains the sizing policies
2088 of each element. Each of the layouts defined in gmtpy is itself a Widget.
2090 Sizing of the widget is controlled by :py:meth:`get_min_size` and
2091 :py:meth:`get_grow` which should be overloaded in derived classes. The
2092 basic behaviour of a Widget instance is to have a vertical and a horizontal
2093 minimum size which default to zero, as well as a vertical and a horizontal
2094 desire to grow, represented by floats, which default to 1.0. Additionally
2095 an aspect ratio constraint may be imposed on the Widget.
2097 After layouting, the widget provides its width, height, x-offset and
2098 y-offset in various ways. Via the Guru interface (see :py:class:`Guru`
2099 class), templates for the -X, -Y and -J option arguments used by GMT
2100 arguments are provided. The defaults are suitable for plotting of linear
2101 (-JX) plots. Other projections can be selected by giving an appropriate 'J'
2102 template, or by manual construction of the -J option, e.g. by utilizing the
2103 :py:meth:`width` and :py:meth:`height` methods. The :py:meth:`bbox` method
2104 can be used to create a PostScript bounding box from the widgets border,
2105 e.g. for use in the :py:meth:`save` method of :py:class:`GMT` instances.
2107 The convention is, that all sizes are given in PostScript points.
2108 Conversion factors are provided as constants :py:const:`inch` and
2109 :py:const:`cm` in the gmtpy module.
2110 '''
2112 def __init__(self, horizontal=None, vertical=None, parent=None):
2114 '''
2115 Create new widget.
2116 '''
2118 Guru.__init__(self)
2120 self.templates = dict(
2121 X='-Xa%(xoffset)gp',
2122 Y='-Ya%(yoffset)gp',
2123 J='-JX%(width)gp/%(height)gp')
2125 if horizontal is None:
2126 self.horizontal = GumSpring()
2127 else:
2128 self.horizontal = horizontal
2130 if vertical is None:
2131 self.vertical = GumSpring()
2132 else:
2133 self.vertical = vertical
2135 self.aspect = None
2136 self.parent = parent
2137 self.dirty = True
2139 def set_parent(self, parent):
2141 '''
2142 Set the parent widget.
2144 This method should not be called directly. The :py:meth:`set_widget`
2145 methods are responsible for calling this.
2146 '''
2148 self.parent = parent
2149 self.dirtyfy()
2151 def get_parent(self):
2153 '''
2154 Get the widgets parent widget.
2155 '''
2157 return self.parent
2159 def get_root(self):
2161 '''
2162 Get the root widget in the layout hierarchy.
2163 '''
2165 if self.parent is not None:
2166 return self.get_parent()
2167 else:
2168 return self
2170 def set_horizontal(self, minimal=None, grow=None):
2172 '''
2173 Set the horizontal sizing policy of the Widget.
2176 :param minimal: new minimal width of the widget
2177 :param grow: new horizontal grow disire of the widget
2178 '''
2180 self.horizontal = GumSpring(minimal, grow)
2181 self.dirtyfy()
2183 def get_horizontal(self):
2184 return self.horizontal.get_minimal(), self.horizontal.get_grow()
2186 def set_vertical(self, minimal=None, grow=None):
2188 '''
2189 Set the horizontal sizing policy of the Widget.
2191 :param minimal: new minimal height of the widget
2192 :param grow: new vertical grow disire of the widget
2193 '''
2195 self.vertical = GumSpring(minimal, grow)
2196 self.dirtyfy()
2198 def get_vertical(self):
2199 return self.vertical.get_minimal(), self.vertical.get_grow()
2201 def set_aspect(self, aspect=None):
2203 '''
2204 Set aspect constraint on the widget.
2206 The aspect is given as height divided by width.
2207 '''
2209 self.aspect = aspect
2210 self.dirtyfy()
2212 def set_policy(self, minimal=(None, None), grow=(None, None), aspect=None):
2214 '''
2215 Shortcut to set sizing and aspect constraints in a single method
2216 call.
2217 '''
2219 self.set_horizontal(minimal[0], grow[0])
2220 self.set_vertical(minimal[1], grow[1])
2221 self.set_aspect(aspect)
2223 def get_policy(self):
2224 mh, gh = self.get_horizontal()
2225 mv, gv = self.get_vertical()
2226 return (mh, mv), (gh, gv), self.aspect
2228 def legalize(self, size, offset):
2230 '''
2231 Get legal size for widget.
2233 Returns: (new_size, new_offset)
2235 Given a box as ``size`` and ``offset``, return ``new_size`` and
2236 ``new_offset``, such that the widget's sizing and aspect constraints
2237 are fullfilled. The returned box is centered on the given input box.
2238 '''
2240 sh, sv = size
2241 oh, ov = offset
2242 shs, svs = Widget.get_min_size(self)
2243 ghs, gvs = Widget.get_grow(self)
2245 if ghs == 0.0:
2246 oh += (sh-shs)/2.
2247 sh = shs
2249 if gvs == 0.0:
2250 ov += (sv-svs)/2.
2251 sv = svs
2253 if self.aspect is not None:
2254 if sh > sv/self.aspect:
2255 oh += (sh-sv/self.aspect)/2.
2256 sh = sv/self.aspect
2257 if sv > sh*self.aspect:
2258 ov += (sv-sh*self.aspect)/2.
2259 sv = sh*self.aspect
2261 return (sh, sv), (oh, ov)
2263 def get_min_size(self):
2265 '''
2266 Get minimum size of widget.
2268 Used by the layout managers. Should be overloaded in derived classes.
2269 '''
2271 mh, mv = self.horizontal.get_minimal(), self.vertical.get_minimal()
2272 if self.aspect is not None:
2273 if mv == 0.0:
2274 return mh, mh*self.aspect
2275 elif mh == 0.0:
2276 return mv/self.aspect, mv
2277 return mh, mv
2279 def get_grow(self):
2281 '''
2282 Get widget's desire to grow.
2284 Used by the layout managers. Should be overloaded in derived classes.
2285 '''
2287 return self.horizontal.get_grow(), self.vertical.get_grow()
2289 def set_size(self, size, offset):
2291 '''
2292 Set the widget's current size.
2294 Should not be called directly. It is the layout manager's
2295 responsibility to call this.
2296 '''
2298 (sh, sv), inner_offset = self.legalize(size, offset)
2299 self.offset = inner_offset
2300 self.horizontal.set_value(sh)
2301 self.vertical.set_value(sv)
2302 self.dirty = False
2304 def __str__(self):
2306 def indent(ind, str):
2307 return ('\n'+ind).join(str.splitlines())
2308 size, offset = self.get_size()
2309 s = "%s (%g x %g) (%g, %g)\n" % ((self.__class__,) + size + offset)
2310 children = self.get_children()
2311 if children:
2312 s += '\n'.join([' ' + indent(' ', str(c)) for c in children])
2313 return s
2315 def policies_debug_str(self):
2317 def indent(ind, str):
2318 return ('\n'+ind).join(str.splitlines())
2319 mins, grows, aspect = self.get_policy()
2320 s = "%s: minimum=(%s, %s), grow=(%s, %s), aspect=%s\n" % (
2321 (self.__class__,) + mins+grows+(aspect,))
2323 children = self.get_children()
2324 if children:
2325 s += '\n'.join([' ' + indent(
2326 ' ', c.policies_debug_str()) for c in children])
2327 return s
2329 def get_corners(self, descend=False):
2331 '''
2332 Get coordinates of the corners of the widget.
2334 Returns list with coordinate tuples.
2336 If ``descend`` is True, the returned list will contain corner
2337 coordinates of all sub-widgets.
2338 '''
2340 self.do_layout()
2341 (sh, sv), (oh, ov) = self.get_size()
2342 corners = [(oh, ov), (oh+sh, ov), (oh+sh, ov+sv), (oh, ov+sv)]
2343 if descend:
2344 for child in self.get_children():
2345 corners.extend(child.get_corners(descend=True))
2346 return corners
2348 def get_sizes(self):
2350 '''
2351 Get sizes of this widget and all it's children.
2353 Returns a list with size tuples.
2354 '''
2355 self.do_layout()
2356 sizes = [self.get_size()]
2357 for child in self.get_children():
2358 sizes.extend(child.get_sizes())
2359 return sizes
2361 def do_layout(self):
2363 '''
2364 Triggers layouting of the widget hierarchy, if needed.
2365 '''
2367 if self.parent is not None:
2368 return self.parent.do_layout()
2370 if not self.dirty:
2371 return
2373 sh, sv = self.get_min_size()
2374 gh, gv = self.get_grow()
2375 if sh == 0.0 and gh != 0.0:
2376 sh = 15.*cm
2377 if sv == 0.0 and gv != 0.0:
2378 sv = 15.*cm*gv/gh * 1./golden_ratio
2379 self.set_size((sh, sv), (0., 0.))
2381 def get_children(self):
2383 '''
2384 Get sub-widgets contained in this widget.
2386 Returns a list of widgets.
2387 '''
2389 return []
2391 def get_size(self):
2393 '''
2394 Get current size and position of the widget.
2396 Triggers layouting and returns
2397 ``((width, height), (xoffset, yoffset))``
2398 '''
2400 self.do_layout()
2401 return (self.horizontal.get_value(),
2402 self.vertical.get_value()), self.offset
2404 def get_params(self):
2406 '''
2407 Get current size and position of the widget.
2409 Triggers layouting and returns dict with keys ``'xoffset'``,
2410 ``'yoffset'``, ``'width'`` and ``'height'``.
2411 '''
2413 self.do_layout()
2414 (w, h), (xo, yo) = self.get_size()
2415 return dict(xoffset=xo, yoffset=yo, width=w, height=h,
2416 width_m=w/_units['m'])
2418 def width(self):
2420 '''
2421 Get current width of the widget.
2423 Triggers layouting and returns width.
2424 '''
2426 self.do_layout()
2427 return self.horizontal.get_value()
2429 def height(self):
2431 '''
2432 Get current height of the widget.
2434 Triggers layouting and return height.
2435 '''
2437 self.do_layout()
2438 return self.vertical.get_value()
2440 def bbox(self):
2442 '''
2443 Get PostScript bounding box for this widget.
2445 Triggers layouting and returns values suitable to create PS bounding
2446 box, representing the widgets current size and position.
2447 '''
2449 self.do_layout()
2450 return (self.offset[0], self.offset[1], self.offset[0]+self.width(),
2451 self.offset[1]+self.height())
2453 def dirtyfy(self):
2455 '''
2456 Set dirty flag on top level widget in the hierarchy.
2458 Called by various methods, to indicate, that the widget hierarchy needs
2459 new layouting.
2460 '''
2462 if self.parent is not None:
2463 self.parent.dirtyfy()
2465 self.dirty = True
2468class CenterLayout(Widget):
2470 '''
2471 A layout manager which centers its single child widget.
2473 The child widget may be oversized.
2474 '''
2476 def __init__(self, horizontal=None, vertical=None):
2477 Widget.__init__(self, horizontal, vertical)
2478 self.content = Widget(horizontal=GumSpring(grow=1.),
2479 vertical=GumSpring(grow=1.), parent=self)
2481 def get_min_size(self):
2482 shs, svs = Widget.get_min_size(self)
2483 sh, sv = self.content.get_min_size()
2484 return max(shs, sh), max(svs, sv)
2486 def get_grow(self):
2487 ghs, gvs = Widget.get_grow(self)
2488 gh, gv = self.content.get_grow()
2489 return gh*ghs, gv*gvs
2491 def set_size(self, size, offset):
2492 (sh, sv), (oh, ov) = self.legalize(size, offset)
2494 shc, svc = self.content.get_min_size()
2495 ghc, gvc = self.content.get_grow()
2496 if ghc != 0.:
2497 shc = sh
2498 if gvc != 0.:
2499 svc = sv
2500 ohc = oh+(sh-shc)/2.
2501 ovc = ov+(sv-svc)/2.
2503 self.content.set_size((shc, svc), (ohc, ovc))
2504 Widget.set_size(self, (sh, sv), (oh, ov))
2506 def set_widget(self, widget=None):
2508 '''
2509 Set the child widget, which shall be centered.
2510 '''
2512 if widget is None:
2513 widget = Widget()
2515 self.content = widget
2517 widget.set_parent(self)
2519 def get_widget(self):
2520 return self.content
2522 def get_children(self):
2523 return [self.content]
2526class FrameLayout(Widget):
2528 '''
2529 A layout manager containing a center widget sorrounded by four margin
2530 widgets.
2532 ::
2534 +---------------------------+
2535 | top |
2536 +---------------------------+
2537 | | | |
2538 | left | center | right |
2539 | | | |
2540 +---------------------------+
2541 | bottom |
2542 +---------------------------+
2544 This layout manager does a little bit of extra effort to maintain the
2545 aspect constraint of the center widget, if this is set. It does so, by
2546 allowing for a bit more flexibility in the sizing of the margins. Two
2547 shortcut methods are provided to set the margin sizes in one shot:
2548 :py:meth:`set_fixed_margins` and :py:meth:`set_min_margins`. The first sets
2549 the margins to fixed sizes, while the second gives them a minimal size and
2550 a (neglectably) small desire to grow. Using the latter may be useful when
2551 setting an aspect constraint on the center widget, because this way the
2552 maximum size of the center widget may be controlled without creating empty
2553 spaces between the widgets.
2554 '''
2556 def __init__(self, horizontal=None, vertical=None):
2557 Widget.__init__(self, horizontal, vertical)
2558 mw = 3.*cm
2559 self.left = Widget(
2560 horizontal=GumSpring(grow=0.15, minimal=mw), parent=self)
2561 self.right = Widget(
2562 horizontal=GumSpring(grow=0.15, minimal=mw), parent=self)
2563 self.top = Widget(
2564 vertical=GumSpring(grow=0.15, minimal=mw/golden_ratio),
2565 parent=self)
2566 self.bottom = Widget(
2567 vertical=GumSpring(grow=0.15, minimal=mw/golden_ratio),
2568 parent=self)
2569 self.center = Widget(
2570 horizontal=GumSpring(grow=0.7), vertical=GumSpring(grow=0.7),
2571 parent=self)
2573 def set_fixed_margins(self, left, right, top, bottom):
2574 '''
2575 Give margins fixed size constraints.
2576 '''
2578 self.left.set_horizontal(left, 0)
2579 self.right.set_horizontal(right, 0)
2580 self.top.set_vertical(top, 0)
2581 self.bottom.set_vertical(bottom, 0)
2583 def set_min_margins(self, left, right, top, bottom, grow=0.0001):
2584 '''
2585 Give margins a minimal size and the possibility to grow.
2587 The desire to grow is set to a very small number.
2588 '''
2589 self.left.set_horizontal(left, grow)
2590 self.right.set_horizontal(right, grow)
2591 self.top.set_vertical(top, grow)
2592 self.bottom.set_vertical(bottom, grow)
2594 def get_min_size(self):
2595 shs, svs = Widget.get_min_size(self)
2597 sl, sr, st, sb, sc = [x.get_min_size() for x in (
2598 self.left, self.right, self.top, self.bottom, self.center)]
2599 gl, gr, gt, gb, gc = [x.get_grow() for x in (
2600 self.left, self.right, self.top, self.bottom, self.center)]
2602 shsum = sl[0]+sr[0]+sc[0]
2603 svsum = st[1]+sb[1]+sc[1]
2605 # prevent widgets from collapsing
2606 for s, g in ((sl, gl), (sr, gr), (sc, gc)):
2607 if s[0] == 0.0 and g[0] != 0.0:
2608 shsum += 0.1*cm
2610 for s, g in ((st, gt), (sb, gb), (sc, gc)):
2611 if s[1] == 0.0 and g[1] != 0.0:
2612 svsum += 0.1*cm
2614 sh = max(shs, shsum)
2615 sv = max(svs, svsum)
2617 return sh, sv
2619 def get_grow(self):
2620 ghs, gvs = Widget.get_grow(self)
2621 gh = (self.left.get_grow()[0] +
2622 self.right.get_grow()[0] +
2623 self.center.get_grow()[0]) * ghs
2624 gv = (self.top.get_grow()[1] +
2625 self.bottom.get_grow()[1] +
2626 self.center.get_grow()[1]) * gvs
2627 return gh, gv
2629 def set_size(self, size, offset):
2630 (sh, sv), (oh, ov) = self.legalize(size, offset)
2632 sl, sr, st, sb, sc = [x.get_min_size() for x in (
2633 self.left, self.right, self.top, self.bottom, self.center)]
2634 gl, gr, gt, gb, gc = [x.get_grow() for x in (
2635 self.left, self.right, self.top, self.bottom, self.center)]
2637 ah = sh - (sl[0]+sr[0]+sc[0])
2638 av = sv - (st[1]+sb[1]+sc[1])
2640 if ah < 0.0:
2641 raise GmtPyError("Container not wide enough for contents "
2642 "(FrameLayout, available: %g cm, needed: %g cm)"
2643 % (sh/cm, (sl[0]+sr[0]+sc[0])/cm))
2644 if av < 0.0:
2645 raise GmtPyError("Container not high enough for contents "
2646 "(FrameLayout, available: %g cm, needed: %g cm)"
2647 % (sv/cm, (st[1]+sb[1]+sc[1])/cm))
2649 slh, srh, sch = distribute((sl[0], sr[0], sc[0]),
2650 (gl[0], gr[0], gc[0]), ah)
2651 stv, sbv, scv = distribute((st[1], sb[1], sc[1]),
2652 (gt[1], gb[1], gc[1]), av)
2654 if self.center.aspect is not None:
2655 ahm = sh - (sl[0]+sr[0] + scv/self.center.aspect)
2656 avm = sv - (st[1]+sb[1] + sch*self.center.aspect)
2657 if 0.0 < ahm < ah:
2658 slh, srh, sch = distribute(
2659 (sl[0], sr[0], scv/self.center.aspect),
2660 (gl[0], gr[0], 0.0), ahm)
2662 elif 0.0 < avm < av:
2663 stv, sbv, scv = distribute((st[1], sb[1],
2664 sch*self.center.aspect),
2665 (gt[1], gb[1], 0.0), avm)
2667 ah = sh - (slh+srh+sch)
2668 av = sv - (stv+sbv+scv)
2670 oh += ah/2.
2671 ov += av/2.
2672 sh -= ah
2673 sv -= av
2675 self.left.set_size((slh, scv), (oh, ov+sbv))
2676 self.right.set_size((srh, scv), (oh+slh+sch, ov+sbv))
2677 self.top.set_size((sh, stv), (oh, ov+sbv+scv))
2678 self.bottom.set_size((sh, sbv), (oh, ov))
2679 self.center.set_size((sch, scv), (oh+slh, ov+sbv))
2680 Widget.set_size(self, (sh, sv), (oh, ov))
2682 def set_widget(self, which='center', widget=None):
2684 '''
2685 Set one of the sub-widgets.
2687 ``which`` should be one of ``'left'``, ``'right'``, ``'top'``,
2688 ``'bottom'`` or ``'center'``.
2689 '''
2691 if widget is None:
2692 widget = Widget()
2694 if which in ('left', 'right', 'top', 'bottom', 'center'):
2695 self.__dict__[which] = widget
2696 else:
2697 raise GmtPyError('No such sub-widget: %s' % which)
2699 widget.set_parent(self)
2701 def get_widget(self, which='center'):
2703 '''
2704 Get one of the sub-widgets.
2706 ``which`` should be one of ``'left'``, ``'right'``, ``'top'``,
2707 ``'bottom'`` or ``'center'``.
2708 '''
2710 if which in ('left', 'right', 'top', 'bottom', 'center'):
2711 return self.__dict__[which]
2712 else:
2713 raise GmtPyError('No such sub-widget: %s' % which)
2715 def get_children(self):
2716 return [self.left, self.right, self.top, self.bottom, self.center]
2719class GridLayout(Widget):
2721 '''
2722 A layout manager which arranges its sub-widgets in a grid.
2724 The grid spacing is flexible and based on the sizing policies of the
2725 contained sub-widgets. If an equidistant grid is needed, the sizing
2726 policies of the sub-widgets have to be set equally.
2728 The height of each row and the width of each column is derived from the
2729 sizing policy of the largest sub-widget in the row or column in question.
2730 The algorithm is not very sophisticated, so conflicting sizing policies
2731 might not be resolved optimally.
2732 '''
2734 def __init__(self, nx=2, ny=2, horizontal=None, vertical=None):
2736 '''
2737 Create new grid layout with ``nx`` columns and ``ny`` rows.
2738 '''
2740 Widget.__init__(self, horizontal, vertical)
2741 self.grid = []
2742 for iy in range(ny):
2743 row = []
2744 for ix in range(nx):
2745 w = Widget(parent=self)
2746 row.append(w)
2748 self.grid.append(row)
2750 def sub_min_sizes_as_array(self):
2751 esh = num.array(
2752 [[w.get_min_size()[0] for w in row] for row in self.grid],
2753 dtype=float)
2754 esv = num.array(
2755 [[w.get_min_size()[1] for w in row] for row in self.grid],
2756 dtype=float)
2757 return esh, esv
2759 def sub_grows_as_array(self):
2760 egh = num.array(
2761 [[w.get_grow()[0] for w in row] for row in self.grid],
2762 dtype=float)
2763 egv = num.array(
2764 [[w.get_grow()[1] for w in row] for row in self.grid],
2765 dtype=float)
2766 return egh, egv
2768 def get_min_size(self):
2769 sh, sv = Widget.get_min_size(self)
2770 esh, esv = self.sub_min_sizes_as_array()
2771 if esh.size != 0:
2772 sh = max(sh, num.sum(esh.max(0)))
2773 if esv.size != 0:
2774 sv = max(sv, num.sum(esv.max(1)))
2775 return sh, sv
2777 def get_grow(self):
2778 ghs, gvs = Widget.get_grow(self)
2779 egh, egv = self.sub_grows_as_array()
2780 if egh.size != 0:
2781 gh = num.sum(egh.max(0))*ghs
2782 else:
2783 gh = 1.0
2784 if egv.size != 0:
2785 gv = num.sum(egv.max(1))*gvs
2786 else:
2787 gv = 1.0
2788 return gh, gv
2790 def set_size(self, size, offset):
2791 (sh, sv), (oh, ov) = self.legalize(size, offset)
2792 esh, esv = self.sub_min_sizes_as_array()
2793 egh, egv = self.sub_grows_as_array()
2795 # available additional space
2796 empty = esh.size == 0
2798 if not empty:
2799 ah = sh - num.sum(esh.max(0))
2800 av = sv - num.sum(esv.max(1))
2801 else:
2802 av = sv
2803 ah = sh
2805 if ah < 0.0:
2806 raise GmtPyError("Container not wide enough for contents "
2807 "(GridLayout, available: %g cm, needed: %g cm)"
2808 % (sh/cm, (num.sum(esh.max(0)))/cm))
2809 if av < 0.0:
2810 raise GmtPyError("Container not high enough for contents "
2811 "(GridLayout, available: %g cm, needed: %g cm)"
2812 % (sv/cm, (num.sum(esv.max(1)))/cm))
2814 nx, ny = esh.shape
2816 if not empty:
2817 # distribute additional space on rows and columns
2818 # according to grow weights and minimal sizes
2819 gsh = egh.sum(1)[:, num.newaxis].repeat(ny, axis=1)
2820 nesh = esh.copy()
2821 nesh += num.where(gsh > 0.0, ah*egh/gsh, 0.0)
2823 nsh = num.maximum(nesh.max(0), esh.max(0))
2825 gsv = egv.sum(0)[num.newaxis, :].repeat(nx, axis=0)
2826 nesv = esv.copy()
2827 nesv += num.where(gsv > 0.0, av*egv/gsv, 0.0)
2828 nsv = num.maximum(nesv.max(1), esv.max(1))
2830 ah = sh - sum(nsh)
2831 av = sv - sum(nsv)
2833 oh += ah/2.
2834 ov += av/2.
2835 sh -= ah
2836 sv -= av
2838 # resize child widgets
2839 neov = ov + sum(nsv)
2840 for row, nesv in zip(self.grid, nsv):
2841 neov -= nesv
2842 neoh = oh
2843 for w, nesh in zip(row, nsh):
2844 w.set_size((nesh, nesv), (neoh, neov))
2845 neoh += nesh
2847 Widget.set_size(self, (sh, sv), (oh, ov))
2849 def set_widget(self, ix, iy, widget=None):
2851 '''
2852 Set one of the sub-widgets.
2854 Sets the sub-widget in column ``ix`` and row ``iy``. The indices are
2855 counted from zero.
2856 '''
2858 if widget is None:
2859 widget = Widget()
2861 self.grid[iy][ix] = widget
2862 widget.set_parent(self)
2864 def get_widget(self, ix, iy):
2866 '''
2867 Get one of the sub-widgets.
2869 Gets the sub-widget from column ``ix`` and row ``iy``. The indices are
2870 counted from zero.
2871 '''
2873 return self.grid[iy][ix]
2875 def get_children(self):
2876 children = []
2877 for row in self.grid:
2878 children.extend(row)
2880 return children
2883def is_gmt5(version='newest'):
2884 return get_gmt_installation(version)['version'][0] in ['5', '6']
2887def is_gmt6(version='newest'):
2888 return get_gmt_installation(version)['version'][0] in ['6']
2891def aspect_for_projection(gmtversion, *args, **kwargs):
2893 gmt = GMT(version=gmtversion, eps_mode=True)
2895 if gmt.is_gmt5():
2896 gmt.psbasemap('-B+gblack', finish=True, *args, **kwargs)
2897 fn = gmt.tempfilename('test.eps')
2898 gmt.save(fn, crop_eps_mode=True)
2899 with open(fn, 'rb') as f:
2900 s = f.read()
2902 l, b, r, t = get_bbox(s)
2903 else:
2904 gmt.psbasemap('-G0', finish=True, *args, **kwargs)
2905 l, b, r, t = gmt.bbox()
2907 return (t-b)/(r-l)
2910def text_box(
2911 text, font=0, font_size=12., angle=0, gmtversion='newest', **kwargs):
2913 gmt = GMT(version=gmtversion)
2914 if gmt.is_gmt5():
2915 row = [0, 0, text]
2916 farg = ['-F+f%gp,%s,%s+j%s' % (font_size, font, 'black', 'BL')]
2917 else:
2918 row = [0, 0, font_size, 0, font, 'BL', text]
2919 farg = []
2921 gmt.pstext(
2922 in_rows=[row],
2923 finish=True,
2924 R=(0, 1, 0, 1),
2925 J='x10p',
2926 N=True,
2927 *farg,
2928 **kwargs)
2930 fn = gmt.tempfilename() + '.ps'
2931 gmt.save(fn)
2933 (_, stderr) = subprocess.Popen(
2934 ['gs', '-q', '-dNOPAUSE', '-dBATCH', '-r720', '-sDEVICE=bbox', fn],
2935 stderr=subprocess.PIPE).communicate()
2937 dx, dy = None, None
2938 for line in stderr.splitlines():
2939 if line.startswith(b'%%HiResBoundingBox:'):
2940 l, b, r, t = [float(x) for x in line.split()[-4:]]
2941 dx, dy = r-l, t-b
2942 break
2944 return dx, dy
2947class TableLiner(object):
2948 '''
2949 Utility class to turn tables into lines.
2950 '''
2952 def __init__(self, in_columns=None, in_rows=None, encoding='utf-8'):
2953 self.in_columns = in_columns
2954 self.in_rows = in_rows
2955 self.encoding = encoding
2957 def __iter__(self):
2958 if self.in_columns is not None:
2959 for row in zip(*self.in_columns):
2960 yield (' '.join([str(x) for x in row])+'\n').encode(
2961 self.encoding)
2963 if self.in_rows is not None:
2964 for row in self.in_rows:
2965 yield (' '.join([str(x) for x in row])+'\n').encode(
2966 self.encoding)
2969class LineStreamChopper(object):
2970 '''
2971 File-like object to buffer data.
2972 '''
2974 def __init__(self, liner):
2975 self.chopsize = None
2976 self.liner = liner
2977 self.chop_iterator = None
2978 self.closed = False
2980 def _chopiter(self):
2981 buf = BytesIO()
2982 for line in self.liner:
2983 buf.write(line)
2984 buflen = buf.tell()
2985 if self.chopsize is not None and buflen >= self.chopsize:
2986 buf.seek(0)
2987 while buf.tell() <= buflen-self.chopsize:
2988 yield buf.read(self.chopsize)
2990 newbuf = BytesIO()
2991 newbuf.write(buf.read())
2992 buf.close()
2993 buf = newbuf
2995 yield buf.getvalue()
2996 buf.close()
2998 def read(self, size=None):
2999 if self.closed:
3000 raise ValueError('Cannot read from closed LineStreamChopper.')
3001 if self.chop_iterator is None:
3002 self.chopsize = size
3003 self.chop_iterator = self._chopiter()
3005 self.chopsize = size
3006 try:
3007 return next(self.chop_iterator)
3008 except StopIteration:
3009 return ''
3011 def close(self):
3012 self.chopsize = None
3013 self.chop_iterator = None
3014 self.closed = True
3016 def flush(self):
3017 pass
3020font_tab = {
3021 0: 'Helvetica',
3022 1: 'Helvetica-Bold',
3023}
3025font_tab_rev = dict((v, k) for (k, v) in font_tab.items())
3028class GMT(object):
3029 '''
3030 A thin wrapper to GMT command execution.
3032 A dict ``config`` may be given to override some of the default GMT
3033 parameters. The ``version`` argument may be used to select a specific GMT
3034 version, which should be used with this GMT instance. The selected
3035 version of GMT has to be installed on the system, must be supported by
3036 gmtpy and gmtpy must know where to find it.
3038 Each instance of this class is used for the task of producing one PS or PDF
3039 output file.
3041 Output of a series of GMT commands is accumulated in memory and can then be
3042 saved as PS or PDF file using the :py:meth:`save` method.
3044 GMT commands are accessed as method calls to instances of this class. See
3045 the :py:meth:`__getattr__` method for details on how the method's
3046 arguments are translated into options and arguments for the GMT command.
3048 Associated with each instance of this class, a temporary directory is
3049 created, where temporary files may be created, and which is automatically
3050 deleted, when the object is destroyed. The :py:meth:`tempfilename` method
3051 may be used to get a random filename in the instance's temporary directory.
3053 Any .gmtdefaults files are ignored. The GMT class uses a fixed
3054 set of defaults, which may be altered via an argument to the constructor.
3055 If possible, GMT is run in 'isolation mode', which was introduced with GMT
3056 version 4.2.2, by setting `GMT_TMPDIR` to the instance's temporary
3057 directory. With earlier versions of GMT, problems may arise with parallel
3058 execution of more than one GMT instance.
3060 Each instance of the GMT class may pick a specific version of GMT which
3061 shall be used, so that, if multiple versions of GMT are installed on the
3062 system, different versions of GMT can be used simultaneously such that
3063 backward compatibility of the scripts can be maintained.
3065 '''
3067 def __init__(
3068 self,
3069 config=None,
3070 kontinue=None,
3071 version='newest',
3072 config_papersize=None,
3073 eps_mode=False):
3075 self.installation = get_gmt_installation(version)
3076 self.gmt_config = dict(self.installation['defaults'])
3077 self.eps_mode = eps_mode
3078 self._shutil = shutil
3080 if config:
3081 self.gmt_config.update(config)
3083 if config_papersize:
3084 if not isinstance(config_papersize, str):
3085 config_papersize = 'Custom_%ix%i' % (
3086 int(config_papersize[0]), int(config_papersize[1]))
3088 if self.is_gmt5():
3089 self.gmt_config['PS_MEDIA'] = config_papersize
3090 else:
3091 self.gmt_config['PAPER_MEDIA'] = config_papersize
3093 self.tempdir = tempfile.mkdtemp("", "gmtpy-")
3094 self.gmt_config_filename = pjoin(self.tempdir, 'gmt.conf')
3095 self.gen_gmt_config_file(self.gmt_config_filename, self.gmt_config)
3097 if kontinue is not None:
3098 self.load_unfinished(kontinue)
3099 self.needstart = False
3100 else:
3101 self.output = BytesIO()
3102 self.needstart = True
3104 self.finished = False
3106 self.environ = os.environ.copy()
3107 self.environ['GMTHOME'] = self.installation.get('home', '')
3108 # GMT isolation mode: works only properly with GMT version >= 4.2.2
3109 self.environ['GMT_TMPDIR'] = self.tempdir
3111 self.layout = None
3112 self.command_log = []
3113 self.keep_temp_dir = False
3115 def is_gmt5(self):
3116 return self.get_version()[0] in ['5', '6']
3118 def is_gmt6(self):
3119 return self.get_version()[0] in ['6']
3121 def get_version(self):
3122 return self.installation['version']
3124 def get_config(self, key):
3125 return self.gmt_config[key]
3127 def to_points(self, string):
3128 if not string:
3129 return 0
3131 unit = string[-1]
3132 if unit in _units:
3133 return float(string[:-1])/_units[unit]
3134 else:
3135 default_unit = measure_unit(self.gmt_config).lower()[0]
3136 return float(string)/_units[default_unit]
3138 def label_font_size(self):
3139 if self.is_gmt5():
3140 return self.to_points(self.gmt_config['FONT_LABEL'].split(',')[0])
3141 else:
3142 return self.to_points(self.gmt_config['LABEL_FONT_SIZE'])
3144 def label_font(self):
3145 if self.is_gmt5():
3146 return font_tab_rev(self.gmt_config['FONT_LABEL'].split(',')[1])
3147 else:
3148 return self.gmt_config['LABEL_FONT']
3150 def gen_gmt_config_file(self, config_filename, config):
3151 f = open(config_filename, 'wb')
3152 f.write(
3153 ('#\n# GMT %s Defaults file\n'
3154 % self.installation['version']).encode('ascii'))
3156 for k, v in config.items():
3157 f.write(('%s = %s\n' % (k, v)).encode('ascii'))
3158 f.close()
3160 def __del__(self):
3161 if not self.keep_temp_dir:
3162 self._shutil.rmtree(self.tempdir)
3164 def _gmtcommand(self, command, *addargs, **kwargs):
3166 '''
3167 Execute arbitrary GMT command.
3169 See docstring in __getattr__ for details.
3170 '''
3172 in_stream = kwargs.pop('in_stream', None)
3173 in_filename = kwargs.pop('in_filename', None)
3174 in_string = kwargs.pop('in_string', None)
3175 in_columns = kwargs.pop('in_columns', None)
3176 in_rows = kwargs.pop('in_rows', None)
3177 out_stream = kwargs.pop('out_stream', None)
3178 out_filename = kwargs.pop('out_filename', None)
3179 out_discard = kwargs.pop('out_discard', None)
3180 finish = kwargs.pop('finish', False)
3181 suppressdefaults = kwargs.pop('suppress_defaults', False)
3182 config_override = kwargs.pop('config', None)
3184 assert not self.finished
3186 # check for mutual exclusiveness on input and output possibilities
3187 assert (1 >= len(
3188 [x for x in [
3189 in_stream, in_filename, in_string, in_columns, in_rows]
3190 if x is not None]))
3191 assert (1 >= len([x for x in [out_stream, out_filename, out_discard]
3192 if x is not None]))
3194 options = []
3196 gmt_config = self.gmt_config
3197 if not self.is_gmt5():
3198 gmt_config_filename = self.gmt_config_filename
3199 if config_override:
3200 gmt_config = self.gmt_config.copy()
3201 gmt_config.update(config_override)
3202 gmt_config_override_filename = pjoin(
3203 self.tempdir, 'gmtdefaults_override')
3204 self.gen_gmt_config_file(
3205 gmt_config_override_filename, gmt_config)
3206 gmt_config_filename = gmt_config_override_filename
3208 else: # gmt5 needs override variables as --VAR=value
3209 if config_override:
3210 for k, v in config_override.items():
3211 options.append('--%s=%s' % (k, v))
3213 if out_discard:
3214 out_filename = '/dev/null'
3216 out_mustclose = False
3217 if out_filename is not None:
3218 out_mustclose = True
3219 out_stream = open(out_filename, 'wb')
3221 if in_filename is not None:
3222 in_stream = open(in_filename, 'rb')
3224 if in_string is not None:
3225 in_stream = BytesIO(in_string)
3227 encoding_gmt = gmt_config.get(
3228 'PS_CHAR_ENCODING',
3229 gmt_config.get('CHAR_ENCODING', 'ISOLatin1+'))
3231 encoding = encoding_gmt_to_python[encoding_gmt.lower()]
3233 if in_columns is not None or in_rows is not None:
3234 in_stream = LineStreamChopper(TableLiner(in_columns=in_columns,
3235 in_rows=in_rows,
3236 encoding=encoding))
3238 # convert option arguments to strings
3239 for k, v in kwargs.items():
3240 if len(k) > 1:
3241 raise GmtPyError('Found illegal keyword argument "%s" '
3242 'while preparing options for command "%s"'
3243 % (k, command))
3245 if type(v) is bool:
3246 if v:
3247 options.append('-%s' % k)
3248 elif type(v) is tuple or type(v) is list:
3249 options.append('-%s' % k + '/'.join([str(x) for x in v]))
3250 else:
3251 options.append('-%s%s' % (k, str(v)))
3253 # if not redirecting to an external sink, handle -K -O
3254 if out_stream is None:
3255 if not finish:
3256 options.append('-K')
3257 else:
3258 self.finished = True
3260 if not self.needstart:
3261 options.append('-O')
3262 else:
3263 self.needstart = False
3265 out_stream = self.output
3267 # run the command
3268 if self.is_gmt5():
3269 args = [pjoin(self.installation['bin'], 'gmt'), command]
3270 else:
3271 args = [pjoin(self.installation['bin'], command)]
3273 if not os.path.isfile(args[0]):
3274 raise OSError('No such file: %s' % args[0])
3275 args.extend(options)
3276 args.extend(addargs)
3277 if not self.is_gmt5() and not suppressdefaults:
3278 # does not seem to work with GMT 5 (and should not be necessary
3279 args.append('+'+gmt_config_filename)
3281 bs = 2048
3282 p = subprocess.Popen(args, stdin=subprocess.PIPE,
3283 stdout=subprocess.PIPE, bufsize=bs,
3284 env=self.environ)
3285 while True:
3286 cr, cw, cx = select([p.stdout], [p.stdin], [])
3287 if cr:
3288 out_stream.write(p.stdout.read(bs))
3289 if cw:
3290 if in_stream is not None:
3291 data = in_stream.read(bs)
3292 if len(data) == 0:
3293 break
3294 p.stdin.write(data)
3295 else:
3296 break
3297 if not cr and not cw:
3298 break
3300 p.stdin.close()
3302 while True:
3303 data = p.stdout.read(bs)
3304 if len(data) == 0:
3305 break
3306 out_stream.write(data)
3308 p.stdout.close()
3310 retcode = p.wait()
3312 if in_stream is not None:
3313 in_stream.close()
3315 if out_mustclose:
3316 out_stream.close()
3318 if retcode != 0:
3319 self.keep_temp_dir = True
3320 raise GMTError('Command %s returned an error. '
3321 'While executing command:\n%s'
3322 % (command, escape_shell_args(args)))
3324 self.command_log.append(args)
3326 def __getattr__(self, command):
3328 '''
3329 Maps to call self._gmtcommand(command, \\*addargs, \\*\\*kwargs).
3331 Execute arbitrary GMT command.
3333 Run a GMT command and by default append its postscript output to the
3334 output file maintained by the GMT instance on which this method is
3335 called.
3337 Except for a few keyword arguments listed below, any ``kwargs`` and
3338 ``addargs`` are converted into command line options and arguments and
3339 passed to the GMT command. Numbers in keyword arguments are converted
3340 into strings. E.g. ``S=10`` is translated into ``'-S10'``. Tuples of
3341 numbers or strings are converted into strings where the elements of the
3342 tuples are separated by slashes '/'. E.g. ``R=(10, 10, 20, 20)`` is
3343 translated into ``'-R10/10/20/20'``. Options with a boolean argument
3344 are only appended to the GMT command, if their values are True.
3346 If no output redirection is in effect, the -K and -O options are
3347 handled by gmtpy and thus should not be specified. Use
3348 ``out_discard=True`` if you don't want -K or -O beeing added, but are
3349 not interested in the output.
3351 The standard input of the GMT process is fed by data selected with one
3352 of the following ``in_*`` keyword arguments:
3354 =============== =======================================================
3355 ``in_stream`` Data is read from an open file like object.
3356 ``in_filename`` Data is read from the given file.
3357 ``in_string`` String content is dumped to the process.
3358 ``in_columns`` A 2D nested iterable whose elements can be accessed as
3359 ``in_columns[icolumn][irow]`` is converted into an
3360 ascii
3361 table, which is fed to the process.
3362 ``in_rows`` A 2D nested iterable whos elements can be accessed as
3363 ``in_rows[irow][icolumn]`` is converted into an ascii
3364 table, which is fed to the process.
3365 =============== =======================================================
3367 The standard output of the GMT process may be redirected by one of the
3368 following options:
3370 ================= =====================================================
3371 ``out_stream`` Output is fed to an open file like object.
3372 ``out_filename`` Output is dumped to the given file.
3373 ``out_discard`` If True, output is dumped to :file:`/dev/null`.
3374 ================= =====================================================
3376 Additional keyword arguments:
3378 ===================== =================================================
3379 ``config`` Dict with GMT defaults which override the
3380 currently active set of defaults exclusively
3381 during this call.
3382 ``finish`` If True, the postscript file, which is maintained
3383 by the GMT instance is finished, and no further
3384 plotting is allowed.
3385 ``suppress_defaults`` Suppress appending of the ``'+gmtdefaults'``
3386 option to the command.
3387 ===================== =================================================
3389 '''
3391 def f(*args, **kwargs):
3392 return self._gmtcommand(command, *args, **kwargs)
3393 return f
3395 def tempfilename(self, name=None):
3396 '''
3397 Get filename for temporary file in the private temp directory.
3399 If no ``name`` argument is given, a random name is picked. If
3400 ``name`` is given, returns a path ending in that ``name``.
3401 '''
3403 if not name:
3404 name = ''.join(
3405 [random.choice('abcdefghijklmnopqrstuvwxyz')
3406 for i in range(10)])
3408 fn = pjoin(self.tempdir, name)
3409 return fn
3411 def tempfile(self, name=None):
3412 '''
3413 Create and open a file in the private temp directory.
3414 '''
3416 fn = self.tempfilename(name)
3417 f = open(fn, 'wb')
3418 return f, fn
3420 def save_unfinished(self, filename):
3421 out = open(filename, 'wb')
3422 out.write(self.output.getvalue())
3423 out.close()
3425 def load_unfinished(self, filename):
3426 self.output = BytesIO()
3427 self.finished = False
3428 inp = open(filename, 'rb')
3429 self.output.write(inp.read())
3430 inp.close()
3432 def dump(self, ident):
3433 filename = self.tempfilename('breakpoint-%s' % ident)
3434 self.save_unfinished(filename)
3436 def load(self, ident):
3437 filename = self.tempfilename('breakpoint-%s' % ident)
3438 self.load_unfinished(filename)
3440 def save(self, filename=None, bbox=None, resolution=150, oversample=2.,
3441 width=None, height=None, size=None, crop_eps_mode=False,
3442 psconvert=False):
3444 '''
3445 Finish and save figure as PDF, PS or PPM file.
3447 If filename ends with ``'.pdf'`` a PDF file is created by piping the
3448 GMT output through :program:`gmtpy-epstopdf`.
3450 If filename ends with ``'.png'`` a PNG file is created by running
3451 :program:`gmtpy-epstopdf`, :program:`pdftocairo` and
3452 :program:`convert`. ``resolution`` specifies the resolution in DPI for
3453 raster file formats. Rasterization is done at a higher resolution if
3454 ``oversample`` is set to a value higher than one. The output image size
3455 can also be controlled by setting ``width``, ``height`` or ``size``
3456 instead of ``resolution``. When ``size`` is given, the image is scaled
3457 so that ``max(width, height) == size``.
3459 The bounding box is set according to the values given in ``bbox``.
3460 '''
3462 if not self.finished:
3463 self.psxy(R=True, J=True, finish=True)
3465 if filename:
3466 tempfn = pjoin(self.tempdir, 'incomplete')
3467 out = open(tempfn, 'wb')
3468 else:
3469 out = sys.stdout
3471 if bbox and not self.is_gmt5():
3472 out.write(replace_bbox(bbox, self.output.getvalue()))
3473 else:
3474 out.write(self.output.getvalue())
3476 if filename:
3477 out.close()
3479 if filename.endswith('.ps') or (
3480 not self.is_gmt5() and filename.endswith('.eps')):
3482 shutil.move(tempfn, filename)
3483 return
3485 if self.is_gmt5():
3486 if crop_eps_mode:
3487 addarg = ['-A']
3488 else:
3489 addarg = []
3491 subprocess.call(
3492 [pjoin(self.installation['bin'], 'gmt'), 'psconvert',
3493 '-Te', '-F%s' % tempfn, tempfn, ] + addarg)
3495 if bbox:
3496 with open(tempfn + '.eps', 'rb') as fin:
3497 with open(tempfn + '-fixbb.eps', 'wb') as fout:
3498 replace_bbox(bbox, fin, fout)
3500 shutil.move(tempfn + '-fixbb.eps', tempfn + '.eps')
3502 else:
3503 shutil.move(tempfn, tempfn + '.eps')
3505 if filename.endswith('.eps'):
3506 shutil.move(tempfn + '.eps', filename)
3507 return
3509 elif filename.endswith('.pdf'):
3510 if psconvert:
3511 gmt_bin = pjoin(self.installation['bin'], 'gmt')
3512 subprocess.call([gmt_bin, 'psconvert', tempfn + '.eps', '-Tf',
3513 '-F' + filename])
3514 else:
3515 subprocess.call(['gmtpy-epstopdf', '--res=%i' % resolution,
3516 '--outfile=' + filename, tempfn + '.eps'])
3517 else:
3518 subprocess.call([
3519 'gmtpy-epstopdf',
3520 '--res=%i' % (resolution * oversample),
3521 '--outfile=' + tempfn + '.pdf', tempfn + '.eps'])
3523 convert_graph(
3524 tempfn + '.pdf', filename,
3525 resolution=resolution, oversample=oversample,
3526 size=size, width=width, height=height)
3528 def bbox(self):
3529 return get_bbox(self.output.getvalue())
3531 def get_command_log(self):
3532 '''
3533 Get the command log.
3534 '''
3536 return self.command_log
3538 def __str__(self):
3539 s = ''
3540 for com in self.command_log:
3541 s += com[0] + "\n " + "\n ".join(com[1:]) + "\n\n"
3542 return s
3544 def page_size_points(self):
3545 '''
3546 Try to get paper size of output postscript file in points.
3547 '''
3549 pm = paper_media(self.gmt_config).lower()
3550 if pm.endswith('+') or pm.endswith('-'):
3551 pm = pm[:-1]
3553 orient = page_orientation(self.gmt_config).lower()
3555 if pm in all_paper_sizes():
3557 if orient == 'portrait':
3558 return get_paper_size(pm)
3559 else:
3560 return get_paper_size(pm)[1], get_paper_size(pm)[0]
3562 m = re.match(r'custom_([0-9.]+)([cimp]?)x([0-9.]+)([cimp]?)', pm)
3563 if m:
3564 w, uw, h, uh = m.groups()
3565 w, h = float(w), float(h)
3566 if uw:
3567 w *= _units[uw]
3568 if uh:
3569 h *= _units[uh]
3570 if orient == 'portrait':
3571 return w, h
3572 else:
3573 return h, w
3575 return None, None
3577 def default_layout(self, with_palette=False):
3578 '''
3579 Get a default layout for the output page.
3581 One of three different layouts is choosen, depending on the
3582 `PAPER_MEDIA` setting in the GMT configuration dict.
3584 If `PAPER_MEDIA` ends with a ``'+'`` (EPS output is selected), a
3585 :py:class:`FrameLayout` is centered on the page, whose size is
3586 controlled by its center widget's size plus the margins of the
3587 :py:class:`FrameLayout`.
3589 If `PAPER_MEDIA` indicates, that a custom page size is wanted by
3590 starting with ``'Custom_'``, a :py:class:`FrameLayout` is used to fill
3591 the complete page. The center widget's size is then controlled by the
3592 page's size minus the margins of the :py:class:`FrameLayout`.
3594 In any other case, two FrameLayouts are nested, such that the outer
3595 layout attaches a 1 cm (printer) margin around the complete page, and
3596 the inner FrameLayout's center widget takes up as much space as
3597 possible under the constraint, that an aspect ratio of 1/golden_ratio
3598 is preserved.
3600 In any case, a reference to the innermost :py:class:`FrameLayout`
3601 instance is returned. The top-level layout can be accessed by calling
3602 :py:meth:`Widget.get_parent` on the returned layout.
3603 '''
3605 if self.layout is None:
3606 w, h = self.page_size_points()
3608 if w is None or h is None:
3609 raise GmtPyError("Can't determine page size for layout")
3611 pm = paper_media(self.gmt_config).lower()
3613 if with_palette:
3614 palette_layout = GridLayout(3, 1)
3615 spacer = palette_layout.get_widget(1, 0)
3616 palette_widget = palette_layout.get_widget(2, 0)
3617 spacer.set_horizontal(0.5*cm)
3618 palette_widget.set_horizontal(0.5*cm)
3620 if pm.endswith('+') or self.eps_mode:
3621 outer = CenterLayout()
3622 outer.set_policy((w, h), (0., 0.))
3623 inner = FrameLayout()
3624 outer.set_widget(inner)
3625 if with_palette:
3626 inner.set_widget('center', palette_layout)
3627 widget = palette_layout
3628 else:
3629 widget = inner.get_widget('center')
3630 widget.set_policy((w/golden_ratio, 0.), (0., 0.),
3631 aspect=1./golden_ratio)
3632 mw = 3.0*cm
3633 inner.set_fixed_margins(
3634 mw, mw, mw/golden_ratio, mw/golden_ratio)
3635 self.layout = inner
3637 elif pm.startswith('custom_'):
3638 layout = FrameLayout()
3639 layout.set_policy((w, h), (0., 0.))
3640 mw = 3.0*cm
3641 layout.set_min_margins(
3642 mw, mw, mw/golden_ratio, mw/golden_ratio)
3643 if with_palette:
3644 layout.set_widget('center', palette_layout)
3645 self.layout = layout
3646 else:
3647 outer = FrameLayout()
3648 outer.set_policy((w, h), (0., 0.))
3649 outer.set_fixed_margins(1.*cm, 1.*cm, 1.*cm, 1.*cm)
3651 inner = FrameLayout()
3652 outer.set_widget('center', inner)
3653 mw = 3.0*cm
3654 inner.set_min_margins(mw, mw, mw/golden_ratio, mw/golden_ratio)
3655 if with_palette:
3656 inner.set_widget('center', palette_layout)
3657 widget = palette_layout
3658 else:
3659 widget = inner.get_widget('center')
3661 widget.set_aspect(1./golden_ratio)
3663 self.layout = inner
3665 return self.layout
3667 def draw_layout(self, layout):
3668 '''
3669 Use psxy to draw layout; for debugging
3670 '''
3672 # corners = layout.get_corners(descend=True)
3673 rects = num.array(layout.get_sizes(), dtype=float)
3674 rects_wid = rects[:, 0, 0]
3675 rects_hei = rects[:, 0, 1]
3676 rects_center_x = rects[:, 1, 0] + rects_wid*0.5
3677 rects_center_y = rects[:, 1, 1] + rects_hei*0.5
3678 nrects = len(rects)
3679 prects = (rects_center_x, rects_center_y, num.arange(nrects),
3680 num.zeros(nrects), rects_hei, rects_wid)
3682 # points = num.array(corners, dtype=float)
3684 cptfile = self.tempfilename() + '.cpt'
3685 self.makecpt(
3686 C='ocean',
3687 T='%g/%g/%g' % (-nrects, nrects, 1),
3688 Z=True,
3689 out_filename=cptfile, suppress_defaults=True)
3691 bb = layout.bbox()
3692 self.psxy(
3693 in_columns=prects,
3694 C=cptfile,
3695 W='1p',
3696 S='J',
3697 R=(bb[0], bb[2], bb[1], bb[3]),
3698 *layout.XYJ())
3701def simpleconf_to_ax(conf, axname):
3702 c = {}
3703 x = axname
3704 for x in ('', axname):
3705 for k in ('label', 'unit', 'scaled_unit', 'scaled_unit_factor',
3706 'space', 'mode', 'approx_ticks', 'limits', 'masking', 'inc',
3707 'snap'):
3709 if x+k in conf:
3710 c[k] = conf[x+k]
3712 return Ax(**c)
3715class DensityPlotDef(object):
3716 def __init__(self, data, cpt='ocean', tension=0.7, size=(640, 480),
3717 contour=False, method='surface', zscaler=None, **extra):
3718 self.data = data
3719 self.cpt = cpt
3720 self.tension = tension
3721 self.size = size
3722 self.contour = contour
3723 self.method = method
3724 self.zscaler = zscaler
3725 self.extra = extra
3728class TextDef(object):
3729 def __init__(
3730 self,
3731 data,
3732 size=9,
3733 justify='MC',
3734 fontno=0,
3735 offset=(0, 0),
3736 color='black'):
3738 self.data = data
3739 self.size = size
3740 self.justify = justify
3741 self.fontno = fontno
3742 self.offset = offset
3743 self.color = color
3746class Simple(object):
3747 def __init__(self, gmtconfig=None, gmtversion='newest', **simple_config):
3748 self.data = []
3749 self.symbols = []
3750 self.config = copy.deepcopy(simple_config)
3751 self.gmtconfig = gmtconfig
3752 self.density_plot_defs = []
3753 self.text_defs = []
3755 self.gmtversion = gmtversion
3757 self.data_x = []
3758 self.symbols_x = []
3760 self.data_y = []
3761 self.symbols_y = []
3763 self.default_config = {}
3764 self.set_defaults(width=15.*cm,
3765 height=15.*cm / golden_ratio,
3766 margins=(2.*cm, 2.*cm, 2.*cm, 2.*cm),
3767 with_palette=False,
3768 palette_offset=0.5*cm,
3769 palette_width=None,
3770 palette_height=None,
3771 zlabeloffset=2*cm,
3772 draw_layout=False)
3774 self.setup_defaults()
3775 self.fixate_widget_aspect = False
3777 def setup_defaults(self):
3778 pass
3780 def set_defaults(self, **kwargs):
3781 self.default_config.update(kwargs)
3783 def plot(self, data, symbol=''):
3784 self.data.append(data)
3785 self.symbols.append(symbol)
3787 def density_plot(self, data, **kwargs):
3788 dpd = DensityPlotDef(data, **kwargs)
3789 self.density_plot_defs.append(dpd)
3791 def text(self, data, **kwargs):
3792 dpd = TextDef(data, **kwargs)
3793 self.text_defs.append(dpd)
3795 def plot_x(self, data, symbol=''):
3796 self.data_x.append(data)
3797 self.symbols_x.append(symbol)
3799 def plot_y(self, data, symbol=''):
3800 self.data_y.append(data)
3801 self.symbols_y.append(symbol)
3803 def set(self, **kwargs):
3804 self.config.update(kwargs)
3806 def setup_base(self, conf):
3807 w = conf.pop('width')
3808 h = conf.pop('height')
3809 margins = conf.pop('margins')
3811 gmtconfig = {}
3812 if self.gmtconfig is not None:
3813 gmtconfig.update(self.gmtconfig)
3815 gmt = GMT(
3816 version=self.gmtversion,
3817 config=gmtconfig,
3818 config_papersize='Custom_%ix%i' % (w, h))
3820 layout = gmt.default_layout(with_palette=conf['with_palette'])
3821 layout.set_min_margins(*margins)
3822 if conf['with_palette']:
3823 widget = layout.get_widget().get_widget(0, 0)
3824 spacer = layout.get_widget().get_widget(1, 0)
3825 spacer.set_horizontal(conf['palette_offset'])
3826 palette_widget = layout.get_widget().get_widget(2, 0)
3827 if conf['palette_width'] is not None:
3828 palette_widget.set_horizontal(conf['palette_width'])
3829 if conf['palette_height'] is not None:
3830 palette_widget.set_vertical(conf['palette_height'])
3831 widget.set_vertical(h-margins[2]-margins[3]-0.03*cm)
3832 return gmt, layout, widget, palette_widget
3833 else:
3834 widget = layout.get_widget()
3835 return gmt, layout, widget, None
3837 def setup_projection(self, widget, scaler, conf):
3838 pass
3840 def setup_scaling(self, conf):
3841 ndims = 2
3842 if self.density_plot_defs:
3843 ndims = 3
3845 axes = [simpleconf_to_ax(conf, x) for x in 'xyz'[:ndims]]
3847 data_all = []
3848 data_all.extend(self.data)
3849 for dsd in self.density_plot_defs:
3850 if dsd.zscaler is None:
3851 data_all.append(dsd.data)
3852 else:
3853 data_all.append(dsd.data[:2])
3854 data_chopped = [ds[:ndims] for ds in data_all]
3856 scaler = ScaleGuru(data_chopped, axes=axes[:ndims])
3858 self.setup_scaling_plus(scaler, axes[:ndims])
3860 return scaler
3862 def setup_scaling_plus(self, scaler, axes):
3863 pass
3865 def setup_scaling_extra(self, scaler, conf):
3867 scaler_x = scaler.copy()
3868 scaler_x.data_ranges[1] = (0., 1.)
3869 scaler_x.axes[1].mode = 'off'
3871 scaler_y = scaler.copy()
3872 scaler_y.data_ranges[0] = (0., 1.)
3873 scaler_y.axes[0].mode = 'off'
3875 return scaler_x, scaler_y
3877 def draw_density(self, gmt, widget, scaler):
3879 R = scaler.R()
3880 # par = scaler.get_params()
3881 rxyj = R + widget.XYJ()
3882 innerticks = False
3883 for dpd in self.density_plot_defs:
3885 fn_cpt = gmt.tempfilename() + '.cpt'
3887 if dpd.zscaler is not None:
3888 s = dpd.zscaler
3889 else:
3890 s = scaler
3892 gmt.makecpt(C=dpd.cpt, out_filename=fn_cpt, *s.T())
3894 fn_grid = gmt.tempfilename()
3896 fn_mean = gmt.tempfilename()
3898 if dpd.method in ('surface', 'triangulate'):
3899 gmt.blockmean(in_columns=dpd.data,
3900 I='%i+/%i+' % dpd.size, # noqa
3901 out_filename=fn_mean, *R)
3903 if dpd.method == 'surface':
3904 gmt.surface(
3905 in_filename=fn_mean,
3906 T=dpd.tension,
3907 G=fn_grid,
3908 I='%i+/%i+' % dpd.size, # noqa
3909 out_discard=True,
3910 *R)
3912 if dpd.method == 'triangulate':
3913 gmt.triangulate(
3914 in_filename=fn_mean,
3915 G=fn_grid,
3916 I='%i+/%i+' % dpd.size, # noqa
3917 out_discard=True,
3918 V=True,
3919 *R)
3921 if gmt.is_gmt5():
3922 gmt.grdimage(fn_grid, C=fn_cpt, E='i', n='l', *rxyj)
3924 else:
3925 gmt.grdimage(fn_grid, C=fn_cpt, E='i', S='l', *rxyj)
3927 if dpd.contour:
3928 gmt.grdcontour(fn_grid, C=fn_cpt, W='0.5p,black', *rxyj)
3929 innerticks = '0.5p,black'
3931 os.remove(fn_grid)
3932 os.remove(fn_mean)
3934 if dpd.method == 'fillcontour':
3935 extra = dict(C=fn_cpt)
3936 extra.update(dpd.extra)
3937 gmt.pscontour(in_columns=dpd.data,
3938 I=True, *rxyj, **extra) # noqa
3940 if dpd.method == 'contour':
3941 extra = dict(W='0.5p,black', C=fn_cpt)
3942 extra.update(dpd.extra)
3943 gmt.pscontour(in_columns=dpd.data, *rxyj, **extra)
3945 return fn_cpt, innerticks
3947 def draw_basemap(self, gmt, widget, scaler):
3948 gmt.psbasemap(*(widget.JXY() + scaler.RB(ax_projection=True)))
3950 def draw(self, gmt, widget, scaler):
3951 rxyj = scaler.R() + widget.JXY()
3952 for dat, sym in zip(self.data, self.symbols):
3953 gmt.psxy(in_columns=dat, *(sym.split()+rxyj))
3955 def post_draw(self, gmt, widget, scaler):
3956 pass
3958 def pre_draw(self, gmt, widget, scaler):
3959 pass
3961 def draw_extra(self, gmt, widget, scaler_x, scaler_y):
3963 for dat, sym in zip(self.data_x, self.symbols_x):
3964 gmt.psxy(in_columns=dat,
3965 *(sym.split() + scaler_x.R() + widget.JXY()))
3967 for dat, sym in zip(self.data_y, self.symbols_y):
3968 gmt.psxy(in_columns=dat,
3969 *(sym.split() + scaler_y.R() + widget.JXY()))
3971 def draw_text(self, gmt, widget, scaler):
3973 rxyj = scaler.R() + widget.JXY()
3974 for td in self.text_defs:
3975 x, y = td.data[0:2]
3976 text = td.data[-1]
3977 size = td.size
3978 angle = 0
3979 fontno = td.fontno
3980 justify = td.justify
3981 color = td.color
3982 if gmt.is_gmt5():
3983 gmt.pstext(
3984 in_rows=[(x, y, text)],
3985 F='+f%gp,%s,%s+a%g+j%s' % (
3986 size, fontno, color, angle, justify),
3987 D='%gp/%gp' % td.offset, *rxyj)
3988 else:
3989 gmt.pstext(
3990 in_rows=[(x, y, size, angle, fontno, justify, text)],
3991 D='%gp/%gp' % td.offset, *rxyj)
3993 def save(self, filename, resolution=150):
3995 conf = dict(self.default_config)
3996 conf.update(self.config)
3998 gmt, layout, widget, palette_widget = self.setup_base(conf)
3999 scaler = self.setup_scaling(conf)
4000 scaler_x, scaler_y = self.setup_scaling_extra(scaler, conf)
4002 self.setup_projection(widget, scaler, conf)
4003 if self.fixate_widget_aspect:
4004 aspect = aspect_for_projection(
4005 gmt.installation['version'], *(widget.J() + scaler.R()))
4007 widget.set_aspect(aspect)
4009 if conf['draw_layout']:
4010 gmt.draw_layout(layout)
4011 cptfile = None
4012 if self.density_plot_defs:
4013 cptfile, innerticks = self.draw_density(gmt, widget, scaler)
4014 self.pre_draw(gmt, widget, scaler)
4015 self.draw(gmt, widget, scaler)
4016 self.post_draw(gmt, widget, scaler)
4017 self.draw_extra(gmt, widget, scaler_x, scaler_y)
4018 self.draw_text(gmt, widget, scaler)
4019 self.draw_basemap(gmt, widget, scaler)
4021 if palette_widget and cptfile:
4022 nice_palette(gmt, palette_widget, scaler, cptfile,
4023 innerticks=innerticks,
4024 zlabeloffset=conf['zlabeloffset'])
4026 gmt.save(filename, resolution=resolution)
4029class LinLinPlot(Simple):
4030 pass
4033class LogLinPlot(Simple):
4035 def setup_defaults(self):
4036 self.set_defaults(xmode='min-max')
4038 def setup_projection(self, widget, scaler, conf):
4039 widget['J'] = '-JX%(width)gpl/%(height)gp'
4040 scaler['B'] = '-B2:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen'
4043class LinLogPlot(Simple):
4045 def setup_defaults(self):
4046 self.set_defaults(ymode='min-max')
4048 def setup_projection(self, widget, scaler, conf):
4049 widget['J'] = '-JX%(width)gp/%(height)gpl'
4050 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/2:%(ylabel)s:WSen'
4053class LogLogPlot(Simple):
4055 def setup_defaults(self):
4056 self.set_defaults(mode='min-max')
4058 def setup_projection(self, widget, scaler, conf):
4059 widget['J'] = '-JX%(width)gpl/%(height)gpl'
4060 scaler['B'] = '-B2:%(xlabel)s:/2:%(ylabel)s:WSen'
4063class AziDistPlot(Simple):
4065 def __init__(self, *args, **kwargs):
4066 Simple.__init__(self, *args, **kwargs)
4067 self.fixate_widget_aspect = True
4069 def setup_defaults(self):
4070 self.set_defaults(
4071 height=15.*cm,
4072 width=15.*cm,
4073 xmode='off',
4074 xlimits=(0., 360.),
4075 xinc=45.)
4077 def setup_projection(self, widget, scaler, conf):
4078 widget['J'] = '-JPa%(width)gp'
4080 def setup_scaling_plus(self, scaler, axes):
4081 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/%(yinc)g:%(ylabel)s:N'
4084class MPlot(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(xmode='min-max', ymode='min-max')
4093 def setup_projection(self, widget, scaler, conf):
4094 par = scaler.get_params()
4095 lon0 = (par['xmin'] + par['xmax'])/2.
4096 lat0 = (par['ymin'] + par['ymax'])/2.
4097 sll = '%g/%g' % (lon0, lat0)
4098 widget['J'] = '-JM' + sll + '/%(width)gp'
4099 scaler['B'] = \
4100 '-B%(xinc)gg%(xinc)g:%(xlabel)s:/%(yinc)gg%(yinc)g:%(ylabel)s:WSen'
4103def nice_palette(gmt, widget, scaleguru, cptfile, zlabeloffset=0.8*inch,
4104 innerticks=True):
4106 par = scaleguru.get_params()
4107 par_ax = scaleguru.get_params(ax_projection=True)
4108 nz_palette = int(widget.height()/inch * 300)
4109 px = num.zeros(nz_palette*2)
4110 px[1::2] += 1
4111 pz = num.linspace(par['zmin'], par['zmax'], nz_palette).repeat(2)
4112 pdz = pz[2]-pz[0]
4113 palgrdfile = gmt.tempfilename()
4114 pal_r = (0, 1, par['zmin'], par['zmax'])
4115 pal_ax_r = (0, 1, par_ax['zmin'], par_ax['zmax'])
4116 gmt.xyz2grd(
4117 G=palgrdfile, R=pal_r,
4118 I=(1, pdz), in_columns=(px, pz, pz), # noqa
4119 out_discard=True)
4121 gmt.grdimage(palgrdfile, R=pal_r, C=cptfile, *widget.JXY())
4122 if isinstance(innerticks, str):
4123 tickpen = innerticks
4124 gmt.grdcontour(palgrdfile, W=tickpen, R=pal_r, C=cptfile,
4125 *widget.JXY())
4127 negpalwid = '%gp' % -widget.width()
4128 if not isinstance(innerticks, str) and innerticks:
4129 ticklen = negpalwid
4130 else:
4131 ticklen = '0p'
4133 TICK_LENGTH_PARAM = 'MAP_TICK_LENGTH' if gmt.is_gmt5() else 'TICK_LENGTH'
4134 gmt.psbasemap(
4135 R=pal_ax_r, B='4::/%(zinc)g::nsw' % par_ax,
4136 config={TICK_LENGTH_PARAM: ticklen},
4137 *widget.JXY())
4139 if innerticks:
4140 gmt.psbasemap(
4141 R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax,
4142 config={TICK_LENGTH_PARAM: '0p'},
4143 *widget.JXY())
4144 else:
4145 gmt.psbasemap(R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax, *widget.JXY())
4147 if par_ax['zlabel']:
4148 label_font = gmt.label_font()
4149 label_font_size = gmt.label_font_size()
4150 label_offset = zlabeloffset
4151 gmt.pstext(
4152 R=(0, 1, 0, 2), D="%gp/0p" % label_offset,
4153 N=True,
4154 in_rows=[(1, 1, label_font_size, -90, label_font, 'CB',
4155 par_ax['zlabel'])],
4156 *widget.JXY())