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.
12from __future__ import print_function, absolute_import
13import subprocess
14try:
15 from StringIO import StringIO as BytesIO
16except ImportError:
17 from io import BytesIO
18import re
19import os
20import sys
21import shutil
22from os.path import join as pjoin
23import tempfile
24import random
25import logging
26import math
27import numpy as num
28import copy
29from select import select
30from scipy.io import netcdf
32from pyrocko import ExternalProgramMissing
33from . import AutoScaler
35try:
36 newstr = unicode
37except NameError:
38 newstr = str
40find_bb = re.compile(br'%%BoundingBox:((\s+[-0-9]+){4})')
41find_hiresbb = re.compile(br'%%HiResBoundingBox:((\s+[-0-9.]+){4})')
44encoding_gmt_to_python = {
45 'isolatin1+': 'iso-8859-1',
46 'standard+': 'ascii',
47 'isolatin1': 'iso-8859-1',
48 'standard': 'ascii'}
50for i in range(1, 11):
51 encoding_gmt_to_python['iso-8859-%i' % i] = 'iso-8859-%i' % i
54def have_gmt():
55 try:
56 get_gmt_installation('newest')
57 return True
59 except GMTInstallationProblem:
60 return False
63def check_have_gmt():
64 if not have_gmt():
65 raise ExternalProgramMissing('GMT is not installed or cannot be found')
68def have_pixmaptools():
69 for prog in [['pdftocairo'], ['convert'], ['gs', '-h']]:
70 try:
71 p = subprocess.Popen(
72 prog,
73 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
75 (stdout, stderr) = p.communicate()
77 except OSError:
78 return False
80 return True
83class GmtPyError(Exception):
84 pass
87class GMTError(GmtPyError):
88 pass
91class GMTInstallationProblem(GmtPyError):
92 pass
95def convert_graph(in_filename, out_filename, resolution=75., oversample=2.,
96 width=None, height=None, size=None):
98 _, tmp_filename_base = tempfile.mkstemp()
100 try:
101 if out_filename.endswith('.svg'):
102 fmt_arg = '-svg'
103 tmp_filename = tmp_filename_base
104 oversample = 1.0
105 else:
106 fmt_arg = '-png'
107 tmp_filename = tmp_filename_base + '-1.png'
109 if size is not None:
110 scale_args = ['-scale-to', '%i' % int(round(size*oversample))]
111 elif width is not None:
112 scale_args = ['-scale-to-x', '%i' % int(round(width*oversample))]
113 elif height is not None:
114 scale_args = ['-scale-to-y', '%i' % int(round(height*oversample))]
115 else:
116 scale_args = ['-r', '%i' % int(round(resolution * oversample))]
118 try:
119 subprocess.check_call(
120 ['pdftocairo'] + scale_args +
121 [fmt_arg, in_filename, tmp_filename_base])
122 except OSError as e:
123 raise GmtPyError(
124 'Cannot start `pdftocairo`, is it installed? (%s)' % str(e))
126 if oversample > 1.:
127 try:
128 subprocess.check_call([
129 'convert',
130 tmp_filename,
131 '-resize', '%i%%' % int(round(100.0/oversample)),
132 out_filename])
133 except OSError as e:
134 raise GmtPyError(
135 'Cannot start `convert`, is it installed? (%s)' % str(e))
137 else:
138 if out_filename.endswith('.png') or out_filename.endswith('.svg'):
139 shutil.move(tmp_filename, out_filename)
140 else:
141 try:
142 subprocess.check_call(
143 ['convert', tmp_filename, out_filename])
144 except Exception as e:
145 raise GmtPyError(
146 'Cannot start `convert`, is it installed? (%s)'
147 % str(e))
149 except Exception:
150 raise
152 finally:
153 if os.path.exists(tmp_filename_base):
154 os.remove(tmp_filename_base)
156 if os.path.exists(tmp_filename):
157 os.remove(tmp_filename)
160def get_bbox(s):
161 for pat in [find_hiresbb, find_bb]:
162 m = pat.search(s)
163 if m:
164 bb = [float(x) for x in m.group(1).split()]
165 return bb
167 raise GmtPyError('Cannot find bbox')
170def replace_bbox(bbox, *args):
172 def repl(m):
173 if m.group(1):
174 return ('%%HiResBoundingBox: ' + ' '.join(
175 '%.3f' % float(x) for x in bbox)).encode('ascii')
176 else:
177 return ('%%%%BoundingBox: %i %i %i %i' % (
178 int(math.floor(bbox[0])),
179 int(math.floor(bbox[1])),
180 int(math.ceil(bbox[2])),
181 int(math.ceil(bbox[3])))).encode('ascii')
183 pat = re.compile(br'%%(HiRes)?BoundingBox:((\s+[0-9.]+){4})')
184 if len(args) == 1:
185 s = args[0]
186 return pat.sub(repl, s)
188 else:
189 fin, fout = args
190 nn = 0
191 for line in fin:
192 line, n = pat.subn(repl, line)
193 nn += n
194 fout.write(line)
195 if nn == 2:
196 break
198 if nn == 2:
199 for line in fin:
200 fout.write(line)
203def escape_shell_arg(s):
204 '''
205 This function should be used for debugging output only - it could be
206 insecure.
207 '''
209 if re.search(r'[^a-zA-Z0-9._/=-]', s):
210 return "'" + s.replace("'", "'\\''") + "'"
211 else:
212 return s
215def escape_shell_args(args):
216 '''
217 This function should be used for debugging output only - it could be
218 insecure.
219 '''
221 return ' '.join([escape_shell_arg(x) for x in args])
224golden_ratio = 1.61803
226# units in points
227_units = {
228 'i': 72.,
229 'c': 72./2.54,
230 'm': 72.*100./2.54,
231 'p': 1.}
233inch = _units['i']
234cm = _units['c']
236# some awsome colors
237tango_colors = {
238 'butter1': (252, 233, 79),
239 'butter2': (237, 212, 0),
240 'butter3': (196, 160, 0),
241 'chameleon1': (138, 226, 52),
242 'chameleon2': (115, 210, 22),
243 'chameleon3': (78, 154, 6),
244 'orange1': (252, 175, 62),
245 'orange2': (245, 121, 0),
246 'orange3': (206, 92, 0),
247 'skyblue1': (114, 159, 207),
248 'skyblue2': (52, 101, 164),
249 'skyblue3': (32, 74, 135),
250 'plum1': (173, 127, 168),
251 'plum2': (117, 80, 123),
252 'plum3': (92, 53, 102),
253 'chocolate1': (233, 185, 110),
254 'chocolate2': (193, 125, 17),
255 'chocolate3': (143, 89, 2),
256 'scarletred1': (239, 41, 41),
257 'scarletred2': (204, 0, 0),
258 'scarletred3': (164, 0, 0),
259 'aluminium1': (238, 238, 236),
260 'aluminium2': (211, 215, 207),
261 'aluminium3': (186, 189, 182),
262 'aluminium4': (136, 138, 133),
263 'aluminium5': (85, 87, 83),
264 'aluminium6': (46, 52, 54)
265}
267graph_colors = [tango_colors[_x] for _x in (
268 'scarletred2', 'skyblue3', 'chameleon3', 'orange2', 'plum2', 'chocolate2',
269 'butter2')]
272def color(x=None):
273 '''
274 Generate a string for GMT option arguments expecting a color.
276 If ``x`` is None, a random color is returned. If it is an integer, the
277 corresponding ``gmtpy.graph_colors[x]`` or black returned. If it is a
278 string and the corresponding ``gmtpy.tango_colors[x]`` exists, this is
279 returned, or the string is passed through. If ``x`` is a tuple, it is
280 transformed into the string form which GMT expects.
281 '''
283 if x is None:
284 return '%i/%i/%i' % tuple(random.randint(0, 255) for _ in 'rgb')
286 if isinstance(x, int):
287 if 0 <= x < len(graph_colors):
288 return '%i/%i/%i' % graph_colors[x]
289 else:
290 return '0/0/0'
292 elif isinstance(x, str):
293 if x in tango_colors:
294 return '%i/%i/%i' % tango_colors[x]
295 else:
296 return x
298 return '%i/%i/%i' % x
301def color_tup(x=None):
302 if x is None:
303 return tuple([random.randint(0, 255) for _x in 'rgb'])
305 if isinstance(x, int):
306 if 0 <= x < len(graph_colors):
307 return graph_colors[x]
308 else:
309 return (0, 0, 0)
311 elif isinstance(x, str):
312 if x in tango_colors:
313 return tango_colors[x]
315 return x
318_gmt_installations = {}
320# Set fixed installation(s) to use...
321# (use this, if you want to use different GMT versions simultaneously.)
323# _gmt_installations['4.2.1'] = {'home': '/sw/etch-ia32/gmt-4.2.1',
324# 'bin': '/sw/etch-ia32/gmt-4.2.1/bin'}
325# _gmt_installations['4.3.0'] = {'home': '/sw/etch-ia32/gmt-4.3.0',
326# 'bin': '/sw/etch-ia32/gmt-4.3.0/bin'}
327# _gmt_installations['6.0.0'] = {'home': '/usr/share/gmt',
328# 'bin': '/usr/bin' }
330# ... or let GmtPy autodetect GMT via $PATH and $GMTHOME
333def key_version(a):
334 a = a.split('_')[0] # get rid of revision id
335 return [int(x) for x in a.split('.')]
338def newest_installed_gmt_version():
339 return sorted(_gmt_installations.keys(), key=key_version)[-1]
342def all_installed_gmt_versions():
343 return sorted(_gmt_installations.keys(), key=key_version)
346# To have consistent defaults, they are hardcoded here and should not be
347# changed.
349_gmt_defaults_by_version = {}
350_gmt_defaults_by_version['4.2.1'] = r'''
351#
352# GMT-SYSTEM 4.2.1 Defaults file
353#
354#-------- Plot Media Parameters -------------
355PAGE_COLOR = 255/255/255
356PAGE_ORIENTATION = portrait
357PAPER_MEDIA = a4+
358#-------- Basemap Annotation Parameters ------
359ANNOT_MIN_ANGLE = 20
360ANNOT_MIN_SPACING = 0
361ANNOT_FONT_PRIMARY = Helvetica
362ANNOT_FONT_SIZE = 12p
363ANNOT_OFFSET_PRIMARY = 0.075i
364ANNOT_FONT_SECONDARY = Helvetica
365ANNOT_FONT_SIZE_SECONDARY = 16p
366ANNOT_OFFSET_SECONDARY = 0.075i
367DEGREE_SYMBOL = ring
368HEADER_FONT = Helvetica
369HEADER_FONT_SIZE = 36p
370HEADER_OFFSET = 0.1875i
371LABEL_FONT = Helvetica
372LABEL_FONT_SIZE = 14p
373LABEL_OFFSET = 0.1125i
374OBLIQUE_ANNOTATION = 1
375PLOT_CLOCK_FORMAT = hh:mm:ss
376PLOT_DATE_FORMAT = yyyy-mm-dd
377PLOT_DEGREE_FORMAT = +ddd:mm:ss
378Y_AXIS_TYPE = hor_text
379#-------- Basemap Layout Parameters ---------
380BASEMAP_AXES = WESN
381BASEMAP_FRAME_RGB = 0/0/0
382BASEMAP_TYPE = plain
383FRAME_PEN = 1.25p
384FRAME_WIDTH = 0.075i
385GRID_CROSS_SIZE_PRIMARY = 0i
386GRID_CROSS_SIZE_SECONDARY = 0i
387GRID_PEN_PRIMARY = 0.25p
388GRID_PEN_SECONDARY = 0.5p
389MAP_SCALE_HEIGHT = 0.075i
390TICK_LENGTH = 0.075i
391POLAR_CAP = 85/90
392TICK_PEN = 0.5p
393X_AXIS_LENGTH = 9i
394Y_AXIS_LENGTH = 6i
395X_ORIGIN = 1i
396Y_ORIGIN = 1i
397UNIX_TIME = FALSE
398UNIX_TIME_POS = -0.75i/-0.75i
399#-------- Color System Parameters -----------
400COLOR_BACKGROUND = 0/0/0
401COLOR_FOREGROUND = 255/255/255
402COLOR_NAN = 128/128/128
403COLOR_IMAGE = adobe
404COLOR_MODEL = rgb
405HSV_MIN_SATURATION = 1
406HSV_MAX_SATURATION = 0.1
407HSV_MIN_VALUE = 0.3
408HSV_MAX_VALUE = 1
409#-------- PostScript Parameters -------------
410CHAR_ENCODING = ISOLatin1+
411DOTS_PR_INCH = 300
412N_COPIES = 1
413PS_COLOR = rgb
414PS_IMAGE_COMPRESS = none
415PS_IMAGE_FORMAT = ascii
416PS_LINE_CAP = round
417PS_LINE_JOIN = miter
418PS_MITER_LIMIT = 35
419PS_VERBOSE = FALSE
420GLOBAL_X_SCALE = 1
421GLOBAL_Y_SCALE = 1
422#-------- I/O Format Parameters -------------
423D_FORMAT = %lg
424FIELD_DELIMITER = tab
425GRIDFILE_SHORTHAND = FALSE
426GRID_FORMAT = nf
427INPUT_CLOCK_FORMAT = hh:mm:ss
428INPUT_DATE_FORMAT = yyyy-mm-dd
429IO_HEADER = FALSE
430N_HEADER_RECS = 1
431OUTPUT_CLOCK_FORMAT = hh:mm:ss
432OUTPUT_DATE_FORMAT = yyyy-mm-dd
433OUTPUT_DEGREE_FORMAT = +D
434XY_TOGGLE = FALSE
435#-------- Projection Parameters -------------
436ELLIPSOID = WGS-84
437MAP_SCALE_FACTOR = default
438MEASURE_UNIT = inch
439#-------- Calendar/Time Parameters ----------
440TIME_FORMAT_PRIMARY = full
441TIME_FORMAT_SECONDARY = full
442TIME_EPOCH = 2000-01-01T00:00:00
443TIME_IS_INTERVAL = OFF
444TIME_INTERVAL_FRACTION = 0.5
445TIME_LANGUAGE = us
446TIME_SYSTEM = other
447TIME_UNIT = d
448TIME_WEEK_START = Sunday
449Y2K_OFFSET_YEAR = 1950
450#-------- Miscellaneous Parameters ----------
451HISTORY = TRUE
452INTERPOLANT = akima
453LINE_STEP = 0.01i
454VECTOR_SHAPE = 0
455VERBOSE = FALSE'''
457_gmt_defaults_by_version['4.3.0'] = r'''
458#
459# GMT-SYSTEM 4.3.0 Defaults file
460#
461#-------- Plot Media Parameters -------------
462PAGE_COLOR = 255/255/255
463PAGE_ORIENTATION = portrait
464PAPER_MEDIA = a4+
465#-------- Basemap Annotation Parameters ------
466ANNOT_MIN_ANGLE = 20
467ANNOT_MIN_SPACING = 0
468ANNOT_FONT_PRIMARY = Helvetica
469ANNOT_FONT_SIZE_PRIMARY = 12p
470ANNOT_OFFSET_PRIMARY = 0.075i
471ANNOT_FONT_SECONDARY = Helvetica
472ANNOT_FONT_SIZE_SECONDARY = 16p
473ANNOT_OFFSET_SECONDARY = 0.075i
474DEGREE_SYMBOL = ring
475HEADER_FONT = Helvetica
476HEADER_FONT_SIZE = 36p
477HEADER_OFFSET = 0.1875i
478LABEL_FONT = Helvetica
479LABEL_FONT_SIZE = 14p
480LABEL_OFFSET = 0.1125i
481OBLIQUE_ANNOTATION = 1
482PLOT_CLOCK_FORMAT = hh:mm:ss
483PLOT_DATE_FORMAT = yyyy-mm-dd
484PLOT_DEGREE_FORMAT = +ddd:mm:ss
485Y_AXIS_TYPE = hor_text
486#-------- Basemap Layout Parameters ---------
487BASEMAP_AXES = WESN
488BASEMAP_FRAME_RGB = 0/0/0
489BASEMAP_TYPE = plain
490FRAME_PEN = 1.25p
491FRAME_WIDTH = 0.075i
492GRID_CROSS_SIZE_PRIMARY = 0i
493GRID_PEN_PRIMARY = 0.25p
494GRID_CROSS_SIZE_SECONDARY = 0i
495GRID_PEN_SECONDARY = 0.5p
496MAP_SCALE_HEIGHT = 0.075i
497POLAR_CAP = 85/90
498TICK_LENGTH = 0.075i
499TICK_PEN = 0.5p
500X_AXIS_LENGTH = 9i
501Y_AXIS_LENGTH = 6i
502X_ORIGIN = 1i
503Y_ORIGIN = 1i
504UNIX_TIME = FALSE
505UNIX_TIME_POS = BL/-0.75i/-0.75i
506UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
507#-------- Color System Parameters -----------
508COLOR_BACKGROUND = 0/0/0
509COLOR_FOREGROUND = 255/255/255
510COLOR_NAN = 128/128/128
511COLOR_IMAGE = adobe
512COLOR_MODEL = rgb
513HSV_MIN_SATURATION = 1
514HSV_MAX_SATURATION = 0.1
515HSV_MIN_VALUE = 0.3
516HSV_MAX_VALUE = 1
517#-------- PostScript Parameters -------------
518CHAR_ENCODING = ISOLatin1+
519DOTS_PR_INCH = 300
520N_COPIES = 1
521PS_COLOR = rgb
522PS_IMAGE_COMPRESS = none
523PS_IMAGE_FORMAT = ascii
524PS_LINE_CAP = round
525PS_LINE_JOIN = miter
526PS_MITER_LIMIT = 35
527PS_VERBOSE = FALSE
528GLOBAL_X_SCALE = 1
529GLOBAL_Y_SCALE = 1
530#-------- I/O Format Parameters -------------
531D_FORMAT = %lg
532FIELD_DELIMITER = tab
533GRIDFILE_SHORTHAND = FALSE
534GRID_FORMAT = nf
535INPUT_CLOCK_FORMAT = hh:mm:ss
536INPUT_DATE_FORMAT = yyyy-mm-dd
537IO_HEADER = FALSE
538N_HEADER_RECS = 1
539OUTPUT_CLOCK_FORMAT = hh:mm:ss
540OUTPUT_DATE_FORMAT = yyyy-mm-dd
541OUTPUT_DEGREE_FORMAT = +D
542XY_TOGGLE = FALSE
543#-------- Projection Parameters -------------
544ELLIPSOID = WGS-84
545MAP_SCALE_FACTOR = default
546MEASURE_UNIT = inch
547#-------- Calendar/Time Parameters ----------
548TIME_FORMAT_PRIMARY = full
549TIME_FORMAT_SECONDARY = full
550TIME_EPOCH = 2000-01-01T00:00:00
551TIME_IS_INTERVAL = OFF
552TIME_INTERVAL_FRACTION = 0.5
553TIME_LANGUAGE = us
554TIME_UNIT = d
555TIME_WEEK_START = Sunday
556Y2K_OFFSET_YEAR = 1950
557#-------- Miscellaneous Parameters ----------
558HISTORY = TRUE
559INTERPOLANT = akima
560LINE_STEP = 0.01i
561VECTOR_SHAPE = 0
562VERBOSE = FALSE'''
565_gmt_defaults_by_version['4.3.1'] = r'''
566#
567# GMT-SYSTEM 4.3.1 Defaults file
568#
569#-------- Plot Media Parameters -------------
570PAGE_COLOR = 255/255/255
571PAGE_ORIENTATION = portrait
572PAPER_MEDIA = a4+
573#-------- Basemap Annotation Parameters ------
574ANNOT_MIN_ANGLE = 20
575ANNOT_MIN_SPACING = 0
576ANNOT_FONT_PRIMARY = Helvetica
577ANNOT_FONT_SIZE_PRIMARY = 12p
578ANNOT_OFFSET_PRIMARY = 0.075i
579ANNOT_FONT_SECONDARY = Helvetica
580ANNOT_FONT_SIZE_SECONDARY = 16p
581ANNOT_OFFSET_SECONDARY = 0.075i
582DEGREE_SYMBOL = ring
583HEADER_FONT = Helvetica
584HEADER_FONT_SIZE = 36p
585HEADER_OFFSET = 0.1875i
586LABEL_FONT = Helvetica
587LABEL_FONT_SIZE = 14p
588LABEL_OFFSET = 0.1125i
589OBLIQUE_ANNOTATION = 1
590PLOT_CLOCK_FORMAT = hh:mm:ss
591PLOT_DATE_FORMAT = yyyy-mm-dd
592PLOT_DEGREE_FORMAT = +ddd:mm:ss
593Y_AXIS_TYPE = hor_text
594#-------- Basemap Layout Parameters ---------
595BASEMAP_AXES = WESN
596BASEMAP_FRAME_RGB = 0/0/0
597BASEMAP_TYPE = plain
598FRAME_PEN = 1.25p
599FRAME_WIDTH = 0.075i
600GRID_CROSS_SIZE_PRIMARY = 0i
601GRID_PEN_PRIMARY = 0.25p
602GRID_CROSS_SIZE_SECONDARY = 0i
603GRID_PEN_SECONDARY = 0.5p
604MAP_SCALE_HEIGHT = 0.075i
605POLAR_CAP = 85/90
606TICK_LENGTH = 0.075i
607TICK_PEN = 0.5p
608X_AXIS_LENGTH = 9i
609Y_AXIS_LENGTH = 6i
610X_ORIGIN = 1i
611Y_ORIGIN = 1i
612UNIX_TIME = FALSE
613UNIX_TIME_POS = BL/-0.75i/-0.75i
614UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
615#-------- Color System Parameters -----------
616COLOR_BACKGROUND = 0/0/0
617COLOR_FOREGROUND = 255/255/255
618COLOR_NAN = 128/128/128
619COLOR_IMAGE = adobe
620COLOR_MODEL = rgb
621HSV_MIN_SATURATION = 1
622HSV_MAX_SATURATION = 0.1
623HSV_MIN_VALUE = 0.3
624HSV_MAX_VALUE = 1
625#-------- PostScript Parameters -------------
626CHAR_ENCODING = ISOLatin1+
627DOTS_PR_INCH = 300
628N_COPIES = 1
629PS_COLOR = rgb
630PS_IMAGE_COMPRESS = none
631PS_IMAGE_FORMAT = ascii
632PS_LINE_CAP = round
633PS_LINE_JOIN = miter
634PS_MITER_LIMIT = 35
635PS_VERBOSE = FALSE
636GLOBAL_X_SCALE = 1
637GLOBAL_Y_SCALE = 1
638#-------- I/O Format Parameters -------------
639D_FORMAT = %lg
640FIELD_DELIMITER = tab
641GRIDFILE_SHORTHAND = FALSE
642GRID_FORMAT = nf
643INPUT_CLOCK_FORMAT = hh:mm:ss
644INPUT_DATE_FORMAT = yyyy-mm-dd
645IO_HEADER = FALSE
646N_HEADER_RECS = 1
647OUTPUT_CLOCK_FORMAT = hh:mm:ss
648OUTPUT_DATE_FORMAT = yyyy-mm-dd
649OUTPUT_DEGREE_FORMAT = +D
650XY_TOGGLE = FALSE
651#-------- Projection Parameters -------------
652ELLIPSOID = WGS-84
653MAP_SCALE_FACTOR = default
654MEASURE_UNIT = inch
655#-------- Calendar/Time Parameters ----------
656TIME_FORMAT_PRIMARY = full
657TIME_FORMAT_SECONDARY = full
658TIME_EPOCH = 2000-01-01T00:00:00
659TIME_IS_INTERVAL = OFF
660TIME_INTERVAL_FRACTION = 0.5
661TIME_LANGUAGE = us
662TIME_UNIT = d
663TIME_WEEK_START = Sunday
664Y2K_OFFSET_YEAR = 1950
665#-------- Miscellaneous Parameters ----------
666HISTORY = TRUE
667INTERPOLANT = akima
668LINE_STEP = 0.01i
669VECTOR_SHAPE = 0
670VERBOSE = FALSE'''
673_gmt_defaults_by_version['4.4.0'] = r'''
674#
675# GMT-SYSTEM 4.4.0 [64-bit] Defaults file
676#
677#-------- Plot Media Parameters -------------
678PAGE_COLOR = 255/255/255
679PAGE_ORIENTATION = portrait
680PAPER_MEDIA = a4+
681#-------- Basemap Annotation Parameters ------
682ANNOT_MIN_ANGLE = 20
683ANNOT_MIN_SPACING = 0
684ANNOT_FONT_PRIMARY = Helvetica
685ANNOT_FONT_SIZE_PRIMARY = 14p
686ANNOT_OFFSET_PRIMARY = 0.075i
687ANNOT_FONT_SECONDARY = Helvetica
688ANNOT_FONT_SIZE_SECONDARY = 16p
689ANNOT_OFFSET_SECONDARY = 0.075i
690DEGREE_SYMBOL = ring
691HEADER_FONT = Helvetica
692HEADER_FONT_SIZE = 36p
693HEADER_OFFSET = 0.1875i
694LABEL_FONT = Helvetica
695LABEL_FONT_SIZE = 14p
696LABEL_OFFSET = 0.1125i
697OBLIQUE_ANNOTATION = 1
698PLOT_CLOCK_FORMAT = hh:mm:ss
699PLOT_DATE_FORMAT = yyyy-mm-dd
700PLOT_DEGREE_FORMAT = +ddd:mm:ss
701Y_AXIS_TYPE = hor_text
702#-------- Basemap Layout Parameters ---------
703BASEMAP_AXES = WESN
704BASEMAP_FRAME_RGB = 0/0/0
705BASEMAP_TYPE = plain
706FRAME_PEN = 1.25p
707FRAME_WIDTH = 0.075i
708GRID_CROSS_SIZE_PRIMARY = 0i
709GRID_PEN_PRIMARY = 0.25p
710GRID_CROSS_SIZE_SECONDARY = 0i
711GRID_PEN_SECONDARY = 0.5p
712MAP_SCALE_HEIGHT = 0.075i
713POLAR_CAP = 85/90
714TICK_LENGTH = 0.075i
715TICK_PEN = 0.5p
716X_AXIS_LENGTH = 9i
717Y_AXIS_LENGTH = 6i
718X_ORIGIN = 1i
719Y_ORIGIN = 1i
720UNIX_TIME = FALSE
721UNIX_TIME_POS = BL/-0.75i/-0.75i
722UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
723#-------- Color System Parameters -----------
724COLOR_BACKGROUND = 0/0/0
725COLOR_FOREGROUND = 255/255/255
726COLOR_NAN = 128/128/128
727COLOR_IMAGE = adobe
728COLOR_MODEL = rgb
729HSV_MIN_SATURATION = 1
730HSV_MAX_SATURATION = 0.1
731HSV_MIN_VALUE = 0.3
732HSV_MAX_VALUE = 1
733#-------- PostScript Parameters -------------
734CHAR_ENCODING = ISOLatin1+
735DOTS_PR_INCH = 300
736N_COPIES = 1
737PS_COLOR = rgb
738PS_IMAGE_COMPRESS = lzw
739PS_IMAGE_FORMAT = ascii
740PS_LINE_CAP = round
741PS_LINE_JOIN = miter
742PS_MITER_LIMIT = 35
743PS_VERBOSE = FALSE
744GLOBAL_X_SCALE = 1
745GLOBAL_Y_SCALE = 1
746#-------- I/O Format Parameters -------------
747D_FORMAT = %lg
748FIELD_DELIMITER = tab
749GRIDFILE_SHORTHAND = FALSE
750GRID_FORMAT = nf
751INPUT_CLOCK_FORMAT = hh:mm:ss
752INPUT_DATE_FORMAT = yyyy-mm-dd
753IO_HEADER = FALSE
754N_HEADER_RECS = 1
755OUTPUT_CLOCK_FORMAT = hh:mm:ss
756OUTPUT_DATE_FORMAT = yyyy-mm-dd
757OUTPUT_DEGREE_FORMAT = +D
758XY_TOGGLE = FALSE
759#-------- Projection Parameters -------------
760ELLIPSOID = WGS-84
761MAP_SCALE_FACTOR = default
762MEASURE_UNIT = inch
763#-------- Calendar/Time Parameters ----------
764TIME_FORMAT_PRIMARY = full
765TIME_FORMAT_SECONDARY = full
766TIME_EPOCH = 2000-01-01T00:00:00
767TIME_IS_INTERVAL = OFF
768TIME_INTERVAL_FRACTION = 0.5
769TIME_LANGUAGE = us
770TIME_UNIT = d
771TIME_WEEK_START = Sunday
772Y2K_OFFSET_YEAR = 1950
773#-------- Miscellaneous Parameters ----------
774HISTORY = TRUE
775INTERPOLANT = akima
776LINE_STEP = 0.01i
777VECTOR_SHAPE = 0
778VERBOSE = FALSE
779'''
781_gmt_defaults_by_version['4.5.2'] = r'''
782#
783# GMT-SYSTEM 4.5.2 [64-bit] Defaults file
784#
785#-------- Plot Media Parameters -------------
786PAGE_COLOR = white
787PAGE_ORIENTATION = portrait
788PAPER_MEDIA = a4+
789#-------- Basemap Annotation Parameters ------
790ANNOT_MIN_ANGLE = 20
791ANNOT_MIN_SPACING = 0
792ANNOT_FONT_PRIMARY = Helvetica
793ANNOT_FONT_SIZE_PRIMARY = 14p
794ANNOT_OFFSET_PRIMARY = 0.075i
795ANNOT_FONT_SECONDARY = Helvetica
796ANNOT_FONT_SIZE_SECONDARY = 16p
797ANNOT_OFFSET_SECONDARY = 0.075i
798DEGREE_SYMBOL = ring
799HEADER_FONT = Helvetica
800HEADER_FONT_SIZE = 36p
801HEADER_OFFSET = 0.1875i
802LABEL_FONT = Helvetica
803LABEL_FONT_SIZE = 14p
804LABEL_OFFSET = 0.1125i
805OBLIQUE_ANNOTATION = 1
806PLOT_CLOCK_FORMAT = hh:mm:ss
807PLOT_DATE_FORMAT = yyyy-mm-dd
808PLOT_DEGREE_FORMAT = +ddd:mm:ss
809Y_AXIS_TYPE = hor_text
810#-------- Basemap Layout Parameters ---------
811BASEMAP_AXES = WESN
812BASEMAP_FRAME_RGB = black
813BASEMAP_TYPE = plain
814FRAME_PEN = 1.25p
815FRAME_WIDTH = 0.075i
816GRID_CROSS_SIZE_PRIMARY = 0i
817GRID_PEN_PRIMARY = 0.25p
818GRID_CROSS_SIZE_SECONDARY = 0i
819GRID_PEN_SECONDARY = 0.5p
820MAP_SCALE_HEIGHT = 0.075i
821POLAR_CAP = 85/90
822TICK_LENGTH = 0.075i
823TICK_PEN = 0.5p
824X_AXIS_LENGTH = 9i
825Y_AXIS_LENGTH = 6i
826X_ORIGIN = 1i
827Y_ORIGIN = 1i
828UNIX_TIME = FALSE
829UNIX_TIME_POS = BL/-0.75i/-0.75i
830UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
831#-------- Color System Parameters -----------
832COLOR_BACKGROUND = black
833COLOR_FOREGROUND = white
834COLOR_NAN = 128
835COLOR_IMAGE = adobe
836COLOR_MODEL = rgb
837HSV_MIN_SATURATION = 1
838HSV_MAX_SATURATION = 0.1
839HSV_MIN_VALUE = 0.3
840HSV_MAX_VALUE = 1
841#-------- PostScript Parameters -------------
842CHAR_ENCODING = ISOLatin1+
843DOTS_PR_INCH = 300
844GLOBAL_X_SCALE = 1
845GLOBAL_Y_SCALE = 1
846N_COPIES = 1
847PS_COLOR = rgb
848PS_IMAGE_COMPRESS = lzw
849PS_IMAGE_FORMAT = ascii
850PS_LINE_CAP = round
851PS_LINE_JOIN = miter
852PS_MITER_LIMIT = 35
853PS_VERBOSE = FALSE
854TRANSPARENCY = 0
855#-------- I/O Format Parameters -------------
856D_FORMAT = %.12lg
857FIELD_DELIMITER = tab
858GRIDFILE_FORMAT = nf
859GRIDFILE_SHORTHAND = FALSE
860INPUT_CLOCK_FORMAT = hh:mm:ss
861INPUT_DATE_FORMAT = yyyy-mm-dd
862IO_HEADER = FALSE
863N_HEADER_RECS = 1
864NAN_RECORDS = pass
865OUTPUT_CLOCK_FORMAT = hh:mm:ss
866OUTPUT_DATE_FORMAT = yyyy-mm-dd
867OUTPUT_DEGREE_FORMAT = D
868XY_TOGGLE = FALSE
869#-------- Projection Parameters -------------
870ELLIPSOID = WGS-84
871MAP_SCALE_FACTOR = default
872MEASURE_UNIT = inch
873#-------- Calendar/Time Parameters ----------
874TIME_FORMAT_PRIMARY = full
875TIME_FORMAT_SECONDARY = full
876TIME_EPOCH = 2000-01-01T00:00:00
877TIME_IS_INTERVAL = OFF
878TIME_INTERVAL_FRACTION = 0.5
879TIME_LANGUAGE = us
880TIME_UNIT = d
881TIME_WEEK_START = Sunday
882Y2K_OFFSET_YEAR = 1950
883#-------- Miscellaneous Parameters ----------
884HISTORY = TRUE
885INTERPOLANT = akima
886LINE_STEP = 0.01i
887VECTOR_SHAPE = 0
888VERBOSE = FALSE
889'''
891_gmt_defaults_by_version['4.5.3'] = r'''
892#
893# GMT-SYSTEM 4.5.3 (CVS Jun 18 2010 10:56:07) [64-bit] Defaults file
894#
895#-------- Plot Media Parameters -------------
896PAGE_COLOR = white
897PAGE_ORIENTATION = portrait
898PAPER_MEDIA = a4+
899#-------- Basemap Annotation Parameters ------
900ANNOT_MIN_ANGLE = 20
901ANNOT_MIN_SPACING = 0
902ANNOT_FONT_PRIMARY = Helvetica
903ANNOT_FONT_SIZE_PRIMARY = 14p
904ANNOT_OFFSET_PRIMARY = 0.075i
905ANNOT_FONT_SECONDARY = Helvetica
906ANNOT_FONT_SIZE_SECONDARY = 16p
907ANNOT_OFFSET_SECONDARY = 0.075i
908DEGREE_SYMBOL = ring
909HEADER_FONT = Helvetica
910HEADER_FONT_SIZE = 36p
911HEADER_OFFSET = 0.1875i
912LABEL_FONT = Helvetica
913LABEL_FONT_SIZE = 14p
914LABEL_OFFSET = 0.1125i
915OBLIQUE_ANNOTATION = 1
916PLOT_CLOCK_FORMAT = hh:mm:ss
917PLOT_DATE_FORMAT = yyyy-mm-dd
918PLOT_DEGREE_FORMAT = +ddd:mm:ss
919Y_AXIS_TYPE = hor_text
920#-------- Basemap Layout Parameters ---------
921BASEMAP_AXES = WESN
922BASEMAP_FRAME_RGB = black
923BASEMAP_TYPE = plain
924FRAME_PEN = 1.25p
925FRAME_WIDTH = 0.075i
926GRID_CROSS_SIZE_PRIMARY = 0i
927GRID_PEN_PRIMARY = 0.25p
928GRID_CROSS_SIZE_SECONDARY = 0i
929GRID_PEN_SECONDARY = 0.5p
930MAP_SCALE_HEIGHT = 0.075i
931POLAR_CAP = 85/90
932TICK_LENGTH = 0.075i
933TICK_PEN = 0.5p
934X_AXIS_LENGTH = 9i
935Y_AXIS_LENGTH = 6i
936X_ORIGIN = 1i
937Y_ORIGIN = 1i
938UNIX_TIME = FALSE
939UNIX_TIME_POS = BL/-0.75i/-0.75i
940UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
941#-------- Color System Parameters -----------
942COLOR_BACKGROUND = black
943COLOR_FOREGROUND = white
944COLOR_NAN = 128
945COLOR_IMAGE = adobe
946COLOR_MODEL = rgb
947HSV_MIN_SATURATION = 1
948HSV_MAX_SATURATION = 0.1
949HSV_MIN_VALUE = 0.3
950HSV_MAX_VALUE = 1
951#-------- PostScript Parameters -------------
952CHAR_ENCODING = ISOLatin1+
953DOTS_PR_INCH = 300
954GLOBAL_X_SCALE = 1
955GLOBAL_Y_SCALE = 1
956N_COPIES = 1
957PS_COLOR = rgb
958PS_IMAGE_COMPRESS = lzw
959PS_IMAGE_FORMAT = ascii
960PS_LINE_CAP = round
961PS_LINE_JOIN = miter
962PS_MITER_LIMIT = 35
963PS_VERBOSE = FALSE
964TRANSPARENCY = 0
965#-------- I/O Format Parameters -------------
966D_FORMAT = %.12lg
967FIELD_DELIMITER = tab
968GRIDFILE_FORMAT = nf
969GRIDFILE_SHORTHAND = FALSE
970INPUT_CLOCK_FORMAT = hh:mm:ss
971INPUT_DATE_FORMAT = yyyy-mm-dd
972IO_HEADER = FALSE
973N_HEADER_RECS = 1
974NAN_RECORDS = pass
975OUTPUT_CLOCK_FORMAT = hh:mm:ss
976OUTPUT_DATE_FORMAT = yyyy-mm-dd
977OUTPUT_DEGREE_FORMAT = D
978XY_TOGGLE = FALSE
979#-------- Projection Parameters -------------
980ELLIPSOID = WGS-84
981MAP_SCALE_FACTOR = default
982MEASURE_UNIT = inch
983#-------- Calendar/Time Parameters ----------
984TIME_FORMAT_PRIMARY = full
985TIME_FORMAT_SECONDARY = full
986TIME_EPOCH = 2000-01-01T00:00:00
987TIME_IS_INTERVAL = OFF
988TIME_INTERVAL_FRACTION = 0.5
989TIME_LANGUAGE = us
990TIME_UNIT = d
991TIME_WEEK_START = Sunday
992Y2K_OFFSET_YEAR = 1950
993#-------- Miscellaneous Parameters ----------
994HISTORY = TRUE
995INTERPOLANT = akima
996LINE_STEP = 0.01i
997VECTOR_SHAPE = 0
998VERBOSE = FALSE
999'''
1001_gmt_defaults_by_version['5.1.2'] = r'''
1002#
1003# GMT 5.1.2 Defaults file
1004# vim:sw=8:ts=8:sts=8
1005# $Revision: 13836 $
1006# $LastChangedDate: 2014-12-20 03:45:42 -1000 (Sat, 20 Dec 2014) $
1007#
1008# COLOR Parameters
1009#
1010COLOR_BACKGROUND = black
1011COLOR_FOREGROUND = white
1012COLOR_NAN = 127.5
1013COLOR_MODEL = none
1014COLOR_HSV_MIN_S = 1
1015COLOR_HSV_MAX_S = 0.1
1016COLOR_HSV_MIN_V = 0.3
1017COLOR_HSV_MAX_V = 1
1018#
1019# DIR Parameters
1020#
1021DIR_DATA =
1022DIR_DCW =
1023DIR_GSHHG =
1024#
1025# FONT Parameters
1026#
1027FONT_ANNOT_PRIMARY = 14p,Helvetica,black
1028FONT_ANNOT_SECONDARY = 16p,Helvetica,black
1029FONT_LABEL = 14p,Helvetica,black
1030FONT_LOGO = 8p,Helvetica,black
1031FONT_TITLE = 24p,Helvetica,black
1032#
1033# FORMAT Parameters
1034#
1035FORMAT_CLOCK_IN = hh:mm:ss
1036FORMAT_CLOCK_OUT = hh:mm:ss
1037FORMAT_CLOCK_MAP = hh:mm:ss
1038FORMAT_DATE_IN = yyyy-mm-dd
1039FORMAT_DATE_OUT = yyyy-mm-dd
1040FORMAT_DATE_MAP = yyyy-mm-dd
1041FORMAT_GEO_OUT = D
1042FORMAT_GEO_MAP = ddd:mm:ss
1043FORMAT_FLOAT_OUT = %.12g
1044FORMAT_FLOAT_MAP = %.12g
1045FORMAT_TIME_PRIMARY_MAP = full
1046FORMAT_TIME_SECONDARY_MAP = full
1047FORMAT_TIME_STAMP = %Y %b %d %H:%M:%S
1048#
1049# GMT Miscellaneous Parameters
1050#
1051GMT_COMPATIBILITY = 4
1052GMT_CUSTOM_LIBS =
1053GMT_EXTRAPOLATE_VAL = NaN
1054GMT_FFT = auto
1055GMT_HISTORY = true
1056GMT_INTERPOLANT = akima
1057GMT_TRIANGULATE = Shewchuk
1058GMT_VERBOSE = compat
1059GMT_LANGUAGE = us
1060#
1061# I/O Parameters
1062#
1063IO_COL_SEPARATOR = tab
1064IO_GRIDFILE_FORMAT = nf
1065IO_GRIDFILE_SHORTHAND = false
1066IO_HEADER = false
1067IO_N_HEADER_RECS = 0
1068IO_NAN_RECORDS = pass
1069IO_NC4_CHUNK_SIZE = auto
1070IO_NC4_DEFLATION_LEVEL = 3
1071IO_LONLAT_TOGGLE = false
1072IO_SEGMENT_MARKER = >
1073#
1074# MAP Parameters
1075#
1076MAP_ANNOT_MIN_ANGLE = 20
1077MAP_ANNOT_MIN_SPACING = 0p
1078MAP_ANNOT_OBLIQUE = 1
1079MAP_ANNOT_OFFSET_PRIMARY = 0.075i
1080MAP_ANNOT_OFFSET_SECONDARY = 0.075i
1081MAP_ANNOT_ORTHO = we
1082MAP_DEFAULT_PEN = default,black
1083MAP_DEGREE_SYMBOL = ring
1084MAP_FRAME_AXES = WESNZ
1085MAP_FRAME_PEN = thicker,black
1086MAP_FRAME_TYPE = fancy
1087MAP_FRAME_WIDTH = 5p
1088MAP_GRID_CROSS_SIZE_PRIMARY = 0p
1089MAP_GRID_CROSS_SIZE_SECONDARY = 0p
1090MAP_GRID_PEN_PRIMARY = default,black
1091MAP_GRID_PEN_SECONDARY = thinner,black
1092MAP_LABEL_OFFSET = 0.1944i
1093MAP_LINE_STEP = 0.75p
1094MAP_LOGO = false
1095MAP_LOGO_POS = BL/-54p/-54p
1096MAP_ORIGIN_X = 1i
1097MAP_ORIGIN_Y = 1i
1098MAP_POLAR_CAP = 85/90
1099MAP_SCALE_HEIGHT = 5p
1100MAP_TICK_LENGTH_PRIMARY = 5p/2.5p
1101MAP_TICK_LENGTH_SECONDARY = 15p/3.75p
1102MAP_TICK_PEN_PRIMARY = thinner,black
1103MAP_TICK_PEN_SECONDARY = thinner,black
1104MAP_TITLE_OFFSET = 14p
1105MAP_VECTOR_SHAPE = 0
1106#
1107# Projection Parameters
1108#
1109PROJ_AUX_LATITUDE = authalic
1110PROJ_ELLIPSOID = WGS-84
1111PROJ_LENGTH_UNIT = cm
1112PROJ_MEAN_RADIUS = authalic
1113PROJ_SCALE_FACTOR = default
1114#
1115# PostScript Parameters
1116#
1117PS_CHAR_ENCODING = ISOLatin1+
1118PS_COLOR_MODEL = rgb
1119PS_COMMENTS = false
1120PS_IMAGE_COMPRESS = deflate,5
1121PS_LINE_CAP = butt
1122PS_LINE_JOIN = miter
1123PS_MITER_LIMIT = 35
1124PS_MEDIA = a4
1125PS_PAGE_COLOR = white
1126PS_PAGE_ORIENTATION = portrait
1127PS_SCALE_X = 1
1128PS_SCALE_Y = 1
1129PS_TRANSPARENCY = Normal
1130#
1131# Calendar/Time Parameters
1132#
1133TIME_EPOCH = 1970-01-01T00:00:00
1134TIME_IS_INTERVAL = off
1135TIME_INTERVAL_FRACTION = 0.5
1136TIME_UNIT = s
1137TIME_WEEK_START = Monday
1138TIME_Y2K_OFFSET_YEAR = 1950
1139'''
1142def get_gmt_version(gmtdefaultsbinary, gmthomedir=None):
1143 args = [gmtdefaultsbinary]
1145 environ = os.environ.copy()
1146 environ['GMTHOME'] = gmthomedir or ''
1148 p = subprocess.Popen(
1149 args,
1150 stdout=subprocess.PIPE,
1151 stderr=subprocess.PIPE,
1152 env=environ)
1154 (stdout, stderr) = p.communicate()
1155 m = re.search(br'(\d+(\.\d+)*)', stderr) \
1156 or re.search(br'# GMT (\d+(\.\d+)*)', stdout)
1158 if not m:
1159 raise GMTInstallationProblem(
1160 "Can't extract version number from output of %s."
1161 % gmtdefaultsbinary)
1163 return str(m.group(1).decode('ascii'))
1166def detect_gmt_installations():
1168 installations = {}
1169 errmesses = []
1171 # GMT 4.x:
1172 try:
1173 p = subprocess.Popen(
1174 ['GMT'],
1175 stdout=subprocess.PIPE,
1176 stderr=subprocess.PIPE)
1178 (stdout, stderr) = p.communicate()
1180 m = re.search(br'Version\s+(\d+(\.\d+)*)', stderr, re.M)
1181 if not m:
1182 raise GMTInstallationProblem(
1183 "Can't get version number from output of GMT.")
1185 version = str(m.group(1).decode('ascii'))
1186 if version[0] != '5':
1188 m = re.search(br'^\s+executables\s+(.+)$', stderr, re.M)
1189 if not m:
1190 raise GMTInstallationProblem(
1191 "Can't extract executables dir from output of GMT.")
1193 gmtbin = str(m.group(1).decode('ascii'))
1195 m = re.search(br'^\s+shared data\s+(.+)$', stderr, re.M)
1196 if not m:
1197 raise GMTInstallationProblem(
1198 "Can't extract shared dir from output of GMT.")
1200 gmtshare = str(m.group(1).decode('ascii'))
1201 if not gmtshare.endswith('/share'):
1202 raise GMTInstallationProblem(
1203 "Can't determine GMTHOME from output of GMT.")
1205 gmthome = gmtshare[:-6]
1207 installations[version] = {
1208 'home': gmthome,
1209 'bin': gmtbin}
1211 except OSError as e:
1212 errmesses.append(('GMT', str(e)))
1214 try:
1215 version = str(subprocess.check_output(
1216 ['gmt', '--version']).strip().decode('ascii')).split('_')[0]
1217 gmtbin = str(subprocess.check_output(
1218 ['gmt', '--show-bindir']).strip().decode('ascii'))
1219 installations[version] = {
1220 'bin': gmtbin}
1222 except (OSError, subprocess.CalledProcessError) as e:
1223 errmesses.append(('gmt', str(e)))
1225 if not installations:
1226 s = []
1227 for (progname, errmess) in errmesses:
1228 s.append('Cannot start "%s" executable: %s' % (progname, errmess))
1230 raise GMTInstallationProblem(', '.join(s))
1232 return installations
1235def appropriate_defaults_version(version):
1236 avails = sorted(_gmt_defaults_by_version.keys(), key=key_version)
1237 for iavail, avail in enumerate(avails):
1238 if key_version(version) == key_version(avail):
1239 return version
1241 elif key_version(version) < key_version(avail):
1242 return avails[max(0, iavail-1)]
1244 return avails[-1]
1247def gmt_default_config(version):
1248 '''
1249 Get default GMT configuration dict for given version.
1250 '''
1252 xversion = appropriate_defaults_version(version)
1254 # if not version in _gmt_defaults_by_version:
1255 # raise GMTError('No GMT defaults for version %s found' % version)
1257 gmt_defaults = _gmt_defaults_by_version[xversion]
1259 d = {}
1260 for line in gmt_defaults.splitlines():
1261 sline = line.strip()
1262 if not sline or sline.startswith('#'):
1263 continue
1265 k, v = sline.split('=', 1)
1266 d[k.strip()] = v.strip()
1268 return d
1271def diff_defaults(v1, v2):
1272 d1 = gmt_default_config(v1)
1273 d2 = gmt_default_config(v2)
1274 for k in d1:
1275 if k not in d2:
1276 print('%s not in %s' % (k, v2))
1277 else:
1278 if d1[k] != d2[k]:
1279 print('%s %s = %s' % (v1, k, d1[k]))
1280 print('%s %s = %s' % (v2, k, d2[k]))
1282 for k in d2:
1283 if k not in d1:
1284 print('%s not in %s' % (k, v1))
1286# diff_defaults('4.5.2', '4.5.3')
1289def check_gmt_installation(installation):
1291 home_dir = installation.get('home', None)
1292 bin_dir = installation['bin']
1293 version = installation['version']
1295 for d in home_dir, bin_dir:
1296 if d is not None:
1297 if not os.path.exists(d):
1298 logging.error(('Directory does not exist: %s\n'
1299 'Check your GMT installation.') % d)
1301 major_version = version.split('.')[0]
1303 if major_version not in ['5', '6']:
1304 gmtdefaults = pjoin(bin_dir, 'gmtdefaults')
1306 versionfound = get_gmt_version(gmtdefaults, home_dir)
1308 if versionfound != version:
1309 raise GMTInstallationProblem((
1310 'Expected GMT version %s but found version %s.\n'
1311 '(Looking at output of %s)') % (
1312 version, versionfound, gmtdefaults))
1315def get_gmt_installation(version):
1316 setup_gmt_installations()
1317 if version != 'newest' and version not in _gmt_installations:
1318 logging.warn('GMT version %s not installed, taking version %s instead'
1319 % (version, newest_installed_gmt_version()))
1321 version = 'newest'
1323 if version == 'newest':
1324 version = newest_installed_gmt_version()
1326 installation = dict(_gmt_installations[version])
1328 return installation
1331def setup_gmt_installations():
1332 if not setup_gmt_installations.have_done:
1333 if not _gmt_installations:
1335 _gmt_installations.update(detect_gmt_installations())
1337 # store defaults as dicts into the gmt installations dicts
1338 for version, installation in _gmt_installations.items():
1339 installation['defaults'] = gmt_default_config(version)
1340 installation['version'] = version
1342 for installation in _gmt_installations.values():
1343 check_gmt_installation(installation)
1345 setup_gmt_installations.have_done = True
1348setup_gmt_installations.have_done = False
1350_paper_sizes_a = '''A0 2380 3368
1351 A1 1684 2380
1352 A2 1190 1684
1353 A3 842 1190
1354 A4 595 842
1355 A5 421 595
1356 A6 297 421
1357 A7 210 297
1358 A8 148 210
1359 A9 105 148
1360 A10 74 105
1361 B0 2836 4008
1362 B1 2004 2836
1363 B2 1418 2004
1364 B3 1002 1418
1365 B4 709 1002
1366 B5 501 709
1367 archA 648 864
1368 archB 864 1296
1369 archC 1296 1728
1370 archD 1728 2592
1371 archE 2592 3456
1372 flsa 612 936
1373 halfletter 396 612
1374 note 540 720
1375 letter 612 792
1376 legal 612 1008
1377 11x17 792 1224
1378 ledger 1224 792'''
1381_paper_sizes = {}
1384def setup_paper_sizes():
1385 if not _paper_sizes:
1386 for line in _paper_sizes_a.splitlines():
1387 k, w, h = line.split()
1388 _paper_sizes[k.lower()] = float(w), float(h)
1391def get_paper_size(k):
1392 setup_paper_sizes()
1393 return _paper_sizes[k.lower().rstrip('+')]
1396def all_paper_sizes():
1397 setup_paper_sizes()
1398 return _paper_sizes
1401def measure_unit(gmt_config):
1402 for k in ['MEASURE_UNIT', 'PROJ_LENGTH_UNIT']:
1403 if k in gmt_config:
1404 return gmt_config[k]
1406 raise GmtPyError('cannot get measure unit / proj length unit from config')
1409def paper_media(gmt_config):
1410 for k in ['PAPER_MEDIA', 'PS_MEDIA']:
1411 if k in gmt_config:
1412 return gmt_config[k]
1414 raise GmtPyError('cannot get paper media from config')
1417def page_orientation(gmt_config):
1418 for k in ['PAGE_ORIENTATION', 'PS_PAGE_ORIENTATION']:
1419 if k in gmt_config:
1420 return gmt_config[k]
1422 raise GmtPyError('cannot get paper orientation from config')
1425def make_bbox(width, height, gmt_config, margins=(0.8, 0.8, 0.8, 0.8)):
1427 leftmargin, topmargin, rightmargin, bottommargin = margins
1428 portrait = page_orientation(gmt_config).lower() == 'portrait'
1430 paper_size = get_paper_size(paper_media(gmt_config))
1431 if not portrait:
1432 paper_size = paper_size[1], paper_size[0]
1434 xoffset = (paper_size[0] - (width + leftmargin + rightmargin)) / \
1435 2.0 + leftmargin
1436 yoffset = (paper_size[1] - (height + topmargin + bottommargin)) / \
1437 2.0 + bottommargin
1439 if portrait:
1440 bb1 = int((xoffset - leftmargin))
1441 bb2 = int((yoffset - bottommargin))
1442 bb3 = bb1 + int((width+leftmargin+rightmargin))
1443 bb4 = bb2 + int((height+topmargin+bottommargin))
1444 else:
1445 bb1 = int((yoffset - topmargin))
1446 bb2 = int((xoffset - leftmargin))
1447 bb3 = bb1 + int((height+topmargin+bottommargin))
1448 bb4 = bb2 + int((width+leftmargin+rightmargin))
1450 return xoffset, yoffset, (bb1, bb2, bb3, bb4)
1453def gmtdefaults_as_text(version='newest'):
1455 '''
1456 Get the built-in gmtdefaults.
1457 '''
1459 if version not in _gmt_installations:
1460 logging.warn('GMT version %s not installed, taking version %s instead'
1461 % (version, newest_installed_gmt_version()))
1462 version = 'newest'
1464 if version == 'newest':
1465 version = newest_installed_gmt_version()
1467 return _gmt_defaults_by_version[version]
1470def savegrd(x, y, z, filename, title=None, naming='xy'):
1471 '''
1472 Write COARDS compliant netcdf (grd) file.
1473 '''
1475 assert y.size, x.size == z.shape
1476 ny, nx = z.shape
1477 nc = netcdf.netcdf_file(filename, 'w')
1478 assert naming in ('xy', 'lonlat')
1480 if naming == 'xy':
1481 kx, ky = 'x', 'y'
1482 else:
1483 kx, ky = 'lon', 'lat'
1485 nc.node_offset = 0
1486 if title is not None:
1487 nc.title = title
1489 nc.Conventions = 'COARDS/CF-1.0'
1490 nc.createDimension(kx, nx)
1491 nc.createDimension(ky, ny)
1493 xvar = nc.createVariable(kx, 'd', (kx,))
1494 yvar = nc.createVariable(ky, 'd', (ky,))
1495 if naming == 'xy':
1496 xvar.long_name = kx
1497 yvar.long_name = ky
1498 else:
1499 xvar.long_name = 'longitude'
1500 xvar.units = 'degrees_east'
1501 yvar.long_name = 'latitude'
1502 yvar.units = 'degrees_north'
1504 zvar = nc.createVariable('z', 'd', (ky, kx))
1506 xvar[:] = x.astype(num.float64)
1507 yvar[:] = y.astype(num.float64)
1508 zvar[:] = z.astype(num.float64)
1510 nc.close()
1513def to_array(var):
1514 arr = var[:].copy()
1515 if hasattr(var, 'scale_factor'):
1516 arr *= var.scale_factor
1518 if hasattr(var, 'add_offset'):
1519 arr += var.add_offset
1521 return arr
1524def loadgrd(filename):
1525 '''
1526 Read COARDS compliant netcdf (grd) file.
1527 '''
1529 nc = netcdf.netcdf_file(filename, 'r')
1530 vkeys = list(nc.variables.keys())
1531 kx = 'x'
1532 ky = 'y'
1533 if 'lon' in vkeys:
1534 kx = 'lon'
1535 if 'lat' in vkeys:
1536 ky = 'lat'
1538 kz = 'z'
1539 if 'altitude' in vkeys:
1540 kz = 'altitude'
1542 x = to_array(nc.variables[kx])
1543 y = to_array(nc.variables[ky])
1544 z = to_array(nc.variables[kz])
1546 nc.close()
1547 return x, y, z
1550def centers_to_edges(asorted):
1551 return (asorted[1:] + asorted[:-1])/2.
1554def nvals(asorted):
1555 eps = (asorted[-1]-asorted[0])/asorted.size
1556 return num.sum(asorted[1:] - asorted[:-1] >= eps) + 1
1559def guess_vals(asorted):
1560 eps = (asorted[-1]-asorted[0])/asorted.size
1561 indis = num.nonzero(asorted[1:] - asorted[:-1] >= eps)[0]
1562 indis = num.concatenate((num.array([0]), indis+1,
1563 num.array([asorted.size])))
1564 asum = num.zeros(asorted.size+1)
1565 asum[1:] = num.cumsum(asorted)
1566 return (asum[indis[1:]] - asum[indis[:-1]]) / (indis[1:]-indis[:-1])
1569def blockmean(asorted, b):
1570 indis = num.nonzero(asorted[1:] - asorted[:-1])[0]
1571 indis = num.concatenate((num.array([0]), indis+1,
1572 num.array([asorted.size])))
1573 bsum = num.zeros(b.size+1)
1574 bsum[1:] = num.cumsum(b)
1575 return (
1576 asorted[indis[:-1]],
1577 (bsum[indis[1:]] - bsum[indis[:-1]]) / (indis[1:]-indis[:-1]))
1580def griddata_regular(x, y, z, xvals, yvals):
1581 nx, ny = xvals.size, yvals.size
1582 xindi = num.digitize(x, centers_to_edges(xvals))
1583 yindi = num.digitize(y, centers_to_edges(yvals))
1585 zindi = yindi*nx+xindi
1586 order = num.argsort(zindi)
1587 z = z[order]
1588 zindi = zindi[order]
1590 zindi, z = blockmean(zindi, z)
1591 znew = num.empty(nx*ny, dtype=float)
1592 znew[:] = num.nan
1593 znew[zindi] = z
1594 return znew.reshape(ny, nx)
1597def guess_field_size(x_sorted, y_sorted, z=None, mode=None):
1598 critical_fraction = 1./num.e - 0.014*3
1599 xs = x_sorted
1600 ys = y_sorted
1601 nxs, nys = nvals(xs), nvals(ys)
1602 if mode == 'nonrandom':
1603 return nxs, nys, 0
1604 elif xs.size == nxs*nys:
1605 # exact match
1606 return nxs, nys, 0
1607 elif nxs >= xs.size*critical_fraction and nys >= xs.size*critical_fraction:
1608 # possibly randomly sampled
1609 nxs = int(math.sqrt(xs.size))
1610 nys = nxs
1611 return nxs, nys, 2
1612 else:
1613 return nxs, nys, 1
1616def griddata_auto(x, y, z, mode=None):
1617 '''
1618 Grid tabular XYZ data by binning.
1620 This function does some extra work to guess the size of the grid. This
1621 should work fine if the input values are already defined on an rectilinear
1622 grid, even if data points are missing or duplicated. This routine also
1623 tries to detect a random distribution of input data and in that case
1624 creates a grid of size sqrt(N) x sqrt(N).
1626 The points do not have to be given in any particular order. Grid nodes
1627 without data are assigned the NaN value. If multiple data points map to the
1628 same grid node, their average is assigned to the grid node.
1629 '''
1631 x, y, z = [num.asarray(X) for X in (x, y, z)]
1632 assert x.size == y.size == z.size
1633 xs, ys = num.sort(x), num.sort(y)
1634 nx, ny, badness = guess_field_size(xs, ys, z, mode=mode)
1635 if badness <= 1:
1636 xf = guess_vals(xs)
1637 yf = guess_vals(ys)
1638 zf = griddata_regular(x, y, z, xf, yf)
1639 else:
1640 xf = num.linspace(xs[0], xs[-1], nx)
1641 yf = num.linspace(ys[0], ys[-1], ny)
1642 zf = griddata_regular(x, y, z, xf, yf)
1644 return xf, yf, zf
1647def tabledata(xf, yf, zf):
1648 assert yf.size, xf.size == zf.shape
1649 x = num.tile(xf, yf.size)
1650 y = num.repeat(yf, xf.size)
1651 z = zf.flatten()
1652 return x, y, z
1655def double1d(a):
1656 a2 = num.empty(a.size*2-1)
1657 a2[::2] = a
1658 a2[1::2] = (a[:-1] + a[1:])/2.
1659 return a2
1662def double2d(f):
1663 f2 = num.empty((f.shape[0]*2-1, f.shape[1]*2-1))
1664 f2[:, :] = num.nan
1665 f2[::2, ::2] = f
1666 f2[1::2, ::2] = (f[:-1, :] + f[1:, :])/2.
1667 f2[::2, 1::2] = (f[:, :-1] + f[:, 1:])/2.
1668 f2[1::2, 1::2] = (f[:-1, :-1] + f[1:, :-1] + f[:-1, 1:] + f[1:, 1:])/4.
1669 diag = f2[1::2, 1::2]
1670 diagA = (f[:-1, :-1] + f[1:, 1:]) / 2.
1671 diagB = (f[1:, :-1] + f[:-1, 1:]) / 2.
1672 f2[1::2, 1::2] = num.where(num.isnan(diag), diagA, diag)
1673 f2[1::2, 1::2] = num.where(num.isnan(diag), diagB, diag)
1674 return f2
1677def doublegrid(x, y, z):
1678 x2 = double1d(x)
1679 y2 = double1d(y)
1680 z2 = double2d(z)
1681 return x2, y2, z2
1684class Guru(object):
1685 '''
1686 Abstract base class providing template interpolation, accessible as
1687 attributes.
1689 Classes deriving from this one, have to implement a :py:meth:`get_params`
1690 method, which is called to get a dict to do ordinary
1691 ``"%(key)x"``-substitutions. The deriving class must also provide a dict
1692 with the templates.
1693 '''
1695 def __init__(self):
1696 self.templates = {}
1698 def fill(self, templates, **kwargs):
1699 params = self.get_params(**kwargs)
1700 strings = [t % params for t in templates]
1701 return strings
1703 # hand through templates dict
1704 def __getitem__(self, template_name):
1705 return self.templates[template_name]
1707 def __setitem__(self, template_name, template):
1708 self.templates[template_name] = template
1710 def __contains__(self, template_name):
1711 return template_name in self.templates
1713 def __iter__(self):
1714 return iter(self.templates)
1716 def __len__(self):
1717 return len(self.templates)
1719 def __delitem__(self, template_name):
1720 del(self.templates[template_name])
1722 def _simple_fill(self, template_names, **kwargs):
1723 templates = [self.templates[n] for n in template_names]
1724 return self.fill(templates, **kwargs)
1726 def __getattr__(self, template_names):
1727 if [n for n in template_names if n not in self.templates]:
1728 raise AttributeError(template_names)
1730 def f(**kwargs):
1731 return self._simple_fill(template_names, **kwargs)
1733 return f
1736class Ax(AutoScaler):
1737 '''
1738 Ax description with autoscaling capabilities.
1740 The ax is described by the :py:class:`pyrocko.plot.AutoScaler`
1741 public attributes, plus the following additional attributes
1742 (with default values given in paranthesis):
1744 .. py:attribute:: label
1746 Ax label (without unit).
1748 .. py:attribute:: unit
1750 Physical unit of the data attached to this ax.
1752 .. py:attribute:: scaled_unit
1754 (see below)
1756 .. py:attribute:: scaled_unit_factor
1758 Scaled physical unit and factor between unit and scaled_unit so that
1760 unit = scaled_unit_factor x scaled_unit.
1762 (E.g. if unit is 'm' and data is in the range of nanometers, you may
1763 want to set the scaled_unit to 'nm' and the scaled_unit_factor to
1764 1e9.)
1766 .. py:attribute:: limits
1768 If defined, fix range of ax to limits=(min,max).
1770 .. py:attribute:: masking
1772 If true and if there is a limit on the ax, while calculating ranges,
1773 the data points are masked such that data points outside of this axes
1774 limits are not used to determine the range of another dependant ax.
1776 '''
1778 def __init__(self, label='', unit='', scaled_unit_factor=1.,
1779 scaled_unit='', limits=None, masking=True, **kwargs):
1781 AutoScaler.__init__(self, **kwargs)
1782 self.label = label
1783 self.unit = unit
1784 self.scaled_unit_factor = scaled_unit_factor
1785 self.scaled_unit = scaled_unit
1786 self.limits = limits
1787 self.masking = masking
1789 def label_str(self, exp, unit):
1790 '''
1791 Get label string including the unit and multiplier.
1792 '''
1794 slabel, sunit, sexp = '', '', ''
1795 if self.label:
1796 slabel = self.label
1798 if unit or exp != 0:
1799 if exp != 0:
1800 sexp = '\\327 10@+%i@+' % exp
1801 sunit = '[ %s %s ]' % (sexp, unit)
1802 else:
1803 sunit = '[ %s ]' % unit
1805 p = []
1806 if slabel:
1807 p.append(slabel)
1809 if sunit:
1810 p.append(sunit)
1812 return ' '.join(p)
1814 def make_params(self, data_range, ax_projection=False, override_mode=None,
1815 override_scaled_unit_factor=None):
1817 '''
1818 Get minimum, maximum, increment and label string for ax display.'
1820 Returns minimum, maximum, increment and label string including unit and
1821 multiplier for given data range.
1823 If ``ax_projection`` is True, values suitable to be displayed on the ax
1824 are returned, e.g. min, max and inc are returned in scaled units.
1825 Otherwise the values are returned in the original units, without any
1826 scaling applied.
1827 '''
1829 sf = self.scaled_unit_factor
1831 if override_scaled_unit_factor is not None:
1832 sf = override_scaled_unit_factor
1834 dr_scaled = [sf*x for x in data_range]
1836 mi, ma, inc = self.make_scale(dr_scaled, override_mode=override_mode)
1837 if self.inc is not None:
1838 inc = self.inc*sf
1840 if ax_projection:
1841 exp = self.make_exp(inc)
1842 if sf == 1. and override_scaled_unit_factor is None:
1843 unit = self.unit
1844 else:
1845 unit = self.scaled_unit
1846 label = self.label_str(exp, unit)
1847 return mi/10**exp, ma/10**exp, inc/10**exp, label
1848 else:
1849 label = self.label_str(0, self.unit)
1850 return mi/sf, ma/sf, inc/sf, label
1853class ScaleGuru(Guru):
1855 '''
1856 2D/3D autoscaling and ax annotation facility.
1858 Instances of this class provide automatic determination of plot ranges,
1859 tick increments and scaled annotations, as well as label/unit handling. It
1860 can in particular be used to automatically generate the -R and -B option
1861 arguments, which are required for most GMT commands.
1863 It extends the functionality of the :py:class:`Ax` and
1864 :py:class:`AutoScaler` classes at the level, where it can not be handled
1865 anymore by looking at a single dimension of the dataset's data, e.g.:
1867 * The ability to impose a fixed aspect ratio between two axes.
1869 * Recalculation of data range on non-limited axes, when there are
1870 limits imposed on other axes.
1872 '''
1874 def __init__(self, data_tuples=None, axes=None, aspect=None,
1875 percent_interval=None, copy_from=None):
1877 Guru.__init__(self)
1879 if copy_from:
1880 self.templates = copy.deepcopy(copy_from.templates)
1881 self.axes = copy.deepcopy(copy_from.axes)
1882 self.data_ranges = copy.deepcopy(copy_from.data_ranges)
1883 self.aspect = copy_from.aspect
1885 if percent_interval is not None:
1886 from scipy.stats import scoreatpercentile as scap
1888 self.templates = dict(
1889 R='-R%(xmin)g/%(xmax)g/%(ymin)g/%(ymax)g',
1890 B='-B%(xinc)g:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen',
1891 T='-T%(zmin)g/%(zmax)g/%(zinc)g')
1893 maxdim = 2
1894 if data_tuples:
1895 maxdim = max(maxdim, max([len(dt) for dt in data_tuples]))
1896 else:
1897 if axes:
1898 maxdim = len(axes)
1899 data_tuples = [([],) * maxdim]
1900 if axes is not None:
1901 self.axes = axes
1902 else:
1903 self.axes = [Ax() for i in range(maxdim)]
1905 # sophisticated data-range calculation
1906 data_ranges = [None] * maxdim
1907 for dt_ in data_tuples:
1908 dt = num.asarray(dt_)
1909 in_range = True
1910 for ax, x in zip(self.axes, dt):
1911 if ax.limits and ax.masking:
1912 ax_limits = list(ax.limits)
1913 if ax_limits[0] is None:
1914 ax_limits[0] = -num.inf
1915 if ax_limits[1] is None:
1916 ax_limits[1] = num.inf
1917 in_range = num.logical_and(
1918 in_range,
1919 num.logical_and(ax_limits[0] <= x, x <= ax_limits[1]))
1921 for i, ax, x in zip(range(maxdim), self.axes, dt):
1923 if not ax.limits or None in ax.limits:
1924 if len(x) >= 1:
1925 if in_range is not True:
1926 xmasked = num.where(in_range, x, num.NaN)
1927 if percent_interval is None:
1928 range_this = (
1929 num.nanmin(xmasked),
1930 num.nanmax(xmasked))
1931 else:
1932 xmasked_finite = num.compress(
1933 num.isfinite(xmasked), xmasked)
1934 range_this = (
1935 scap(xmasked_finite,
1936 (100.-percent_interval)/2.),
1937 scap(xmasked_finite,
1938 100.-(100.-percent_interval)/2.))
1939 else:
1940 if percent_interval is None:
1941 range_this = num.nanmin(x), num.nanmax(x)
1942 else:
1943 xmasked_finite = num.compress(
1944 num.isfinite(xmasked), xmasked)
1945 range_this = (
1946 scap(xmasked_finite,
1947 (100.-percent_interval)/2.),
1948 scap(xmasked_finite,
1949 100.-(100.-percent_interval)/2.))
1950 else:
1951 range_this = (0., 1.)
1953 if ax.limits:
1954 if ax.limits[0] is not None:
1955 range_this = ax.limits[0], max(ax.limits[0],
1956 range_this[1])
1958 if ax.limits[1] is not None:
1959 range_this = min(ax.limits[1],
1960 range_this[0]), ax.limits[1]
1962 else:
1963 range_this = ax.limits
1965 if data_ranges[i] is None and range_this[0] <= range_this[1]:
1966 data_ranges[i] = range_this
1967 else:
1968 mi, ma = range_this
1969 if data_ranges[i] is not None:
1970 mi = min(data_ranges[i][0], mi)
1971 ma = max(data_ranges[i][1], ma)
1973 data_ranges[i] = (mi, ma)
1975 for i in range(len(data_ranges)):
1976 if data_ranges[i] is None or not (
1977 num.isfinite(data_ranges[i][0])
1978 and num.isfinite(data_ranges[i][1])):
1980 data_ranges[i] = (0., 1.)
1982 self.data_ranges = data_ranges
1983 self.aspect = aspect
1985 def copy(self):
1986 return ScaleGuru(copy_from=self)
1988 def get_params(self, ax_projection=False):
1990 '''
1991 Get dict with output parameters.
1993 For each data dimension, ax minimum, maximum, increment and a label
1994 string (including unit and exponential factor) are determined. E.g. in
1995 for the first dimension the output dict will contain the keys
1996 ``'xmin'``, ``'xmax'``, ``'xinc'``, and ``'xlabel'``.
1998 Normally, values corresponding to the scaling of the raw data are
1999 produced, but if ``ax_projection`` is ``True``, values which are
2000 suitable to be printed on the axes are returned. This means that in the
2001 latter case, the :py:attr:`Ax.scaled_unit` and
2002 :py:attr:`Ax.scaled_unit_factor` attributes as set on the axes are
2003 respected and that a common 10^x factor is factored out and put to the
2004 label string.
2005 '''
2007 xmi, xma, xinc, xlabel = self.axes[0].make_params(
2008 self.data_ranges[0], ax_projection)
2009 ymi, yma, yinc, ylabel = self.axes[1].make_params(
2010 self.data_ranges[1], ax_projection)
2011 if len(self.axes) > 2:
2012 zmi, zma, zinc, zlabel = self.axes[2].make_params(
2013 self.data_ranges[2], ax_projection)
2015 # enforce certain aspect, if needed
2016 if self.aspect is not None:
2017 xwid = xma-xmi
2018 ywid = yma-ymi
2019 if ywid < xwid*self.aspect:
2020 ymi -= (xwid*self.aspect - ywid)*0.5
2021 yma += (xwid*self.aspect - ywid)*0.5
2022 ymi, yma, yinc, ylabel = self.axes[1].make_params(
2023 (ymi, yma), ax_projection, override_mode='off',
2024 override_scaled_unit_factor=1.)
2026 elif xwid < ywid/self.aspect:
2027 xmi -= (ywid/self.aspect - xwid)*0.5
2028 xma += (ywid/self.aspect - xwid)*0.5
2029 xmi, xma, xinc, xlabel = self.axes[0].make_params(
2030 (xmi, xma), ax_projection, override_mode='off',
2031 override_scaled_unit_factor=1.)
2033 params = dict(xmin=xmi, xmax=xma, xinc=xinc, xlabel=xlabel,
2034 ymin=ymi, ymax=yma, yinc=yinc, ylabel=ylabel)
2035 if len(self.axes) > 2:
2036 params.update(dict(zmin=zmi, zmax=zma, zinc=zinc, zlabel=zlabel))
2038 return params
2041class GumSpring(object):
2043 '''
2044 Sizing policy implementing a minimal size, plus a desire to grow.
2045 '''
2047 def __init__(self, minimal=None, grow=None):
2048 self.minimal = minimal
2049 if grow is None:
2050 if minimal is None:
2051 self.grow = 1.0
2052 else:
2053 self.grow = 0.0
2054 else:
2055 self.grow = grow
2056 self.value = 1.0
2058 def get_minimal(self):
2059 if self.minimal is not None:
2060 return self.minimal
2061 else:
2062 return 0.0
2064 def get_grow(self):
2065 return self.grow
2067 def set_value(self, value):
2068 self.value = value
2070 def get_value(self):
2071 return self.value
2074def distribute(sizes, grows, space):
2075 sizes = list(sizes)
2076 gsum = sum(grows)
2077 if gsum > 0.0:
2078 for i in range(len(sizes)):
2079 sizes[i] += space*grows[i]/gsum
2080 return sizes
2083class Widget(Guru):
2085 '''
2086 Base class of the gmtpy layout system.
2088 The Widget class provides the basic functionality for the nesting and
2089 placing of elements on the output page, and maintains the sizing policies
2090 of each element. Each of the layouts defined in gmtpy is itself a Widget.
2092 Sizing of the widget is controlled by :py:meth:`get_min_size` and
2093 :py:meth:`get_grow` which should be overloaded in derived classes. The
2094 basic behaviour of a Widget instance is to have a vertical and a horizontal
2095 minimum size which default to zero, as well as a vertical and a horizontal
2096 desire to grow, represented by floats, which default to 1.0. Additionally
2097 an aspect ratio constraint may be imposed on the Widget.
2099 After layouting, the widget provides its width, height, x-offset and
2100 y-offset in various ways. Via the Guru interface (see :py:class:`Guru`
2101 class), templates for the -X, -Y and -J option arguments used by GMT
2102 arguments are provided. The defaults are suitable for plotting of linear
2103 (-JX) plots. Other projections can be selected by giving an appropriate 'J'
2104 template, or by manual construction of the -J option, e.g. by utilizing the
2105 :py:meth:`width` and :py:meth:`height` methods. The :py:meth:`bbox` method
2106 can be used to create a PostScript bounding box from the widgets border,
2107 e.g. for use in the :py:meth:`save` method of :py:class:`GMT` instances.
2109 The convention is, that all sizes are given in PostScript points.
2110 Conversion factors are provided as constants :py:const:`inch` and
2111 :py:const:`cm` in the gmtpy module.
2112 '''
2114 def __init__(self, horizontal=None, vertical=None, parent=None):
2116 '''
2117 Create new widget.
2118 '''
2120 Guru.__init__(self)
2122 self.templates = dict(
2123 X='-Xa%(xoffset)gp',
2124 Y='-Ya%(yoffset)gp',
2125 J='-JX%(width)gp/%(height)gp')
2127 if horizontal is None:
2128 self.horizontal = GumSpring()
2129 else:
2130 self.horizontal = horizontal
2132 if vertical is None:
2133 self.vertical = GumSpring()
2134 else:
2135 self.vertical = vertical
2137 self.aspect = None
2138 self.parent = parent
2139 self.dirty = True
2141 def set_parent(self, parent):
2143 '''
2144 Set the parent widget.
2146 This method should not be called directly. The :py:meth:`set_widget`
2147 methods are responsible for calling this.
2148 '''
2150 self.parent = parent
2151 self.dirtyfy()
2153 def get_parent(self):
2155 '''
2156 Get the widgets parent widget.
2157 '''
2159 return self.parent
2161 def get_root(self):
2163 '''
2164 Get the root widget in the layout hierarchy.
2165 '''
2167 if self.parent is not None:
2168 return self.get_parent()
2169 else:
2170 return self
2172 def set_horizontal(self, minimal=None, grow=None):
2174 '''
2175 Set the horizontal sizing policy of the Widget.
2178 :param minimal: new minimal width of the widget
2179 :param grow: new horizontal grow disire of the widget
2180 '''
2182 self.horizontal = GumSpring(minimal, grow)
2183 self.dirtyfy()
2185 def get_horizontal(self):
2186 return self.horizontal.get_minimal(), self.horizontal.get_grow()
2188 def set_vertical(self, minimal=None, grow=None):
2190 '''
2191 Set the horizontal sizing policy of the Widget.
2193 :param minimal: new minimal height of the widget
2194 :param grow: new vertical grow disire of the widget
2195 '''
2197 self.vertical = GumSpring(minimal, grow)
2198 self.dirtyfy()
2200 def get_vertical(self):
2201 return self.vertical.get_minimal(), self.vertical.get_grow()
2203 def set_aspect(self, aspect=None):
2205 '''
2206 Set aspect constraint on the widget.
2208 The aspect is given as height divided by width.
2209 '''
2211 self.aspect = aspect
2212 self.dirtyfy()
2214 def set_policy(self, minimal=(None, None), grow=(None, None), aspect=None):
2216 '''
2217 Shortcut to set sizing and aspect constraints in a single method
2218 call.
2219 '''
2221 self.set_horizontal(minimal[0], grow[0])
2222 self.set_vertical(minimal[1], grow[1])
2223 self.set_aspect(aspect)
2225 def get_policy(self):
2226 mh, gh = self.get_horizontal()
2227 mv, gv = self.get_vertical()
2228 return (mh, mv), (gh, gv), self.aspect
2230 def legalize(self, size, offset):
2232 '''
2233 Get legal size for widget.
2235 Returns: (new_size, new_offset)
2237 Given a box as ``size`` and ``offset``, return ``new_size`` and
2238 ``new_offset``, such that the widget's sizing and aspect constraints
2239 are fullfilled. The returned box is centered on the given input box.
2240 '''
2242 sh, sv = size
2243 oh, ov = offset
2244 shs, svs = Widget.get_min_size(self)
2245 ghs, gvs = Widget.get_grow(self)
2247 if ghs == 0.0:
2248 oh += (sh-shs)/2.
2249 sh = shs
2251 if gvs == 0.0:
2252 ov += (sv-svs)/2.
2253 sv = svs
2255 if self.aspect is not None:
2256 if sh > sv/self.aspect:
2257 oh += (sh-sv/self.aspect)/2.
2258 sh = sv/self.aspect
2259 if sv > sh*self.aspect:
2260 ov += (sv-sh*self.aspect)/2.
2261 sv = sh*self.aspect
2263 return (sh, sv), (oh, ov)
2265 def get_min_size(self):
2267 '''
2268 Get minimum size of widget.
2270 Used by the layout managers. Should be overloaded in derived classes.
2271 '''
2273 mh, mv = self.horizontal.get_minimal(), self.vertical.get_minimal()
2274 if self.aspect is not None:
2275 if mv == 0.0:
2276 return mh, mh*self.aspect
2277 elif mh == 0.0:
2278 return mv/self.aspect, mv
2279 return mh, mv
2281 def get_grow(self):
2283 '''
2284 Get widget's desire to grow.
2286 Used by the layout managers. Should be overloaded in derived classes.
2287 '''
2289 return self.horizontal.get_grow(), self.vertical.get_grow()
2291 def set_size(self, size, offset):
2293 '''
2294 Set the widget's current size.
2296 Should not be called directly. It is the layout manager's
2297 responsibility to call this.
2298 '''
2300 (sh, sv), inner_offset = self.legalize(size, offset)
2301 self.offset = inner_offset
2302 self.horizontal.set_value(sh)
2303 self.vertical.set_value(sv)
2304 self.dirty = False
2306 def __str__(self):
2308 def indent(ind, str):
2309 return ('\n'+ind).join(str.splitlines())
2310 size, offset = self.get_size()
2311 s = "%s (%g x %g) (%g, %g)\n" % ((self.__class__,) + size + offset)
2312 children = self.get_children()
2313 if children:
2314 s += '\n'.join([' ' + indent(' ', str(c)) for c in children])
2315 return s
2317 def policies_debug_str(self):
2319 def indent(ind, str):
2320 return ('\n'+ind).join(str.splitlines())
2321 mins, grows, aspect = self.get_policy()
2322 s = "%s: minimum=(%s, %s), grow=(%s, %s), aspect=%s\n" % (
2323 (self.__class__,) + mins+grows+(aspect,))
2325 children = self.get_children()
2326 if children:
2327 s += '\n'.join([' ' + indent(
2328 ' ', c.policies_debug_str()) for c in children])
2329 return s
2331 def get_corners(self, descend=False):
2333 '''
2334 Get coordinates of the corners of the widget.
2336 Returns list with coordinate tuples.
2338 If ``descend`` is True, the returned list will contain corner
2339 coordinates of all sub-widgets.
2340 '''
2342 self.do_layout()
2343 (sh, sv), (oh, ov) = self.get_size()
2344 corners = [(oh, ov), (oh+sh, ov), (oh+sh, ov+sv), (oh, ov+sv)]
2345 if descend:
2346 for child in self.get_children():
2347 corners.extend(child.get_corners(descend=True))
2348 return corners
2350 def get_sizes(self):
2352 '''
2353 Get sizes of this widget and all it's children.
2355 Returns a list with size tuples.
2356 '''
2357 self.do_layout()
2358 sizes = [self.get_size()]
2359 for child in self.get_children():
2360 sizes.extend(child.get_sizes())
2361 return sizes
2363 def do_layout(self):
2365 '''
2366 Triggers layouting of the widget hierarchy, if needed.
2367 '''
2369 if self.parent is not None:
2370 return self.parent.do_layout()
2372 if not self.dirty:
2373 return
2375 sh, sv = self.get_min_size()
2376 gh, gv = self.get_grow()
2377 if sh == 0.0 and gh != 0.0:
2378 sh = 15.*cm
2379 if sv == 0.0 and gv != 0.0:
2380 sv = 15.*cm*gv/gh * 1./golden_ratio
2381 self.set_size((sh, sv), (0., 0.))
2383 def get_children(self):
2385 '''
2386 Get sub-widgets contained in this widget.
2388 Returns a list of widgets.
2389 '''
2391 return []
2393 def get_size(self):
2395 '''
2396 Get current size and position of the widget.
2398 Triggers layouting and returns
2399 ``((width, height), (xoffset, yoffset))``
2400 '''
2402 self.do_layout()
2403 return (self.horizontal.get_value(),
2404 self.vertical.get_value()), self.offset
2406 def get_params(self):
2408 '''
2409 Get current size and position of the widget.
2411 Triggers layouting and returns dict with keys ``'xoffset'``,
2412 ``'yoffset'``, ``'width'`` and ``'height'``.
2413 '''
2415 self.do_layout()
2416 (w, h), (xo, yo) = self.get_size()
2417 return dict(xoffset=xo, yoffset=yo, width=w, height=h,
2418 width_m=w/_units['m'])
2420 def width(self):
2422 '''
2423 Get current width of the widget.
2425 Triggers layouting and returns width.
2426 '''
2428 self.do_layout()
2429 return self.horizontal.get_value()
2431 def height(self):
2433 '''
2434 Get current height of the widget.
2436 Triggers layouting and return height.
2437 '''
2439 self.do_layout()
2440 return self.vertical.get_value()
2442 def bbox(self):
2444 '''
2445 Get PostScript bounding box for this widget.
2447 Triggers layouting and returns values suitable to create PS bounding
2448 box, representing the widgets current size and position.
2449 '''
2451 self.do_layout()
2452 return (self.offset[0], self.offset[1], self.offset[0]+self.width(),
2453 self.offset[1]+self.height())
2455 def dirtyfy(self):
2457 '''
2458 Set dirty flag on top level widget in the hierarchy.
2460 Called by various methods, to indicate, that the widget hierarchy needs
2461 new layouting.
2462 '''
2464 if self.parent is not None:
2465 self.parent.dirtyfy()
2467 self.dirty = True
2470class CenterLayout(Widget):
2472 '''
2473 A layout manager which centers its single child widget.
2475 The child widget may be oversized.
2476 '''
2478 def __init__(self, horizontal=None, vertical=None):
2479 Widget.__init__(self, horizontal, vertical)
2480 self.content = Widget(horizontal=GumSpring(grow=1.),
2481 vertical=GumSpring(grow=1.), parent=self)
2483 def get_min_size(self):
2484 shs, svs = Widget.get_min_size(self)
2485 sh, sv = self.content.get_min_size()
2486 return max(shs, sh), max(svs, sv)
2488 def get_grow(self):
2489 ghs, gvs = Widget.get_grow(self)
2490 gh, gv = self.content.get_grow()
2491 return gh*ghs, gv*gvs
2493 def set_size(self, size, offset):
2494 (sh, sv), (oh, ov) = self.legalize(size, offset)
2496 shc, svc = self.content.get_min_size()
2497 ghc, gvc = self.content.get_grow()
2498 if ghc != 0.:
2499 shc = sh
2500 if gvc != 0.:
2501 svc = sv
2502 ohc = oh+(sh-shc)/2.
2503 ovc = ov+(sv-svc)/2.
2505 self.content.set_size((shc, svc), (ohc, ovc))
2506 Widget.set_size(self, (sh, sv), (oh, ov))
2508 def set_widget(self, widget=None):
2510 '''
2511 Set the child widget, which shall be centered.
2512 '''
2514 if widget is None:
2515 widget = Widget()
2517 self.content = widget
2519 widget.set_parent(self)
2521 def get_widget(self):
2522 return self.content
2524 def get_children(self):
2525 return [self.content]
2528class FrameLayout(Widget):
2530 '''
2531 A layout manager containing a center widget sorrounded by four margin
2532 widgets.
2534 ::
2536 +---------------------------+
2537 | top |
2538 +---------------------------+
2539 | | | |
2540 | left | center | right |
2541 | | | |
2542 +---------------------------+
2543 | bottom |
2544 +---------------------------+
2546 This layout manager does a little bit of extra effort to maintain the
2547 aspect constraint of the center widget, if this is set. It does so, by
2548 allowing for a bit more flexibility in the sizing of the margins. Two
2549 shortcut methods are provided to set the margin sizes in one shot:
2550 :py:meth:`set_fixed_margins` and :py:meth:`set_min_margins`. The first sets
2551 the margins to fixed sizes, while the second gives them a minimal size and
2552 a (neglectably) small desire to grow. Using the latter may be useful when
2553 setting an aspect constraint on the center widget, because this way the
2554 maximum size of the center widget may be controlled without creating empty
2555 spaces between the widgets.
2556 '''
2558 def __init__(self, horizontal=None, vertical=None):
2559 Widget.__init__(self, horizontal, vertical)
2560 mw = 3.*cm
2561 self.left = Widget(
2562 horizontal=GumSpring(grow=0.15, minimal=mw), parent=self)
2563 self.right = Widget(
2564 horizontal=GumSpring(grow=0.15, minimal=mw), parent=self)
2565 self.top = Widget(
2566 vertical=GumSpring(grow=0.15, minimal=mw/golden_ratio),
2567 parent=self)
2568 self.bottom = Widget(
2569 vertical=GumSpring(grow=0.15, minimal=mw/golden_ratio),
2570 parent=self)
2571 self.center = Widget(
2572 horizontal=GumSpring(grow=0.7), vertical=GumSpring(grow=0.7),
2573 parent=self)
2575 def set_fixed_margins(self, left, right, top, bottom):
2576 '''
2577 Give margins fixed size constraints.
2578 '''
2580 self.left.set_horizontal(left, 0)
2581 self.right.set_horizontal(right, 0)
2582 self.top.set_vertical(top, 0)
2583 self.bottom.set_vertical(bottom, 0)
2585 def set_min_margins(self, left, right, top, bottom, grow=0.0001):
2586 '''
2587 Give margins a minimal size and the possibility to grow.
2589 The desire to grow is set to a very small number.
2590 '''
2591 self.left.set_horizontal(left, grow)
2592 self.right.set_horizontal(right, grow)
2593 self.top.set_vertical(top, grow)
2594 self.bottom.set_vertical(bottom, grow)
2596 def get_min_size(self):
2597 shs, svs = Widget.get_min_size(self)
2599 sl, sr, st, sb, sc = [x.get_min_size() for x in (
2600 self.left, self.right, self.top, self.bottom, self.center)]
2601 gl, gr, gt, gb, gc = [x.get_grow() for x in (
2602 self.left, self.right, self.top, self.bottom, self.center)]
2604 shsum = sl[0]+sr[0]+sc[0]
2605 svsum = st[1]+sb[1]+sc[1]
2607 # prevent widgets from collapsing
2608 for s, g in ((sl, gl), (sr, gr), (sc, gc)):
2609 if s[0] == 0.0 and g[0] != 0.0:
2610 shsum += 0.1*cm
2612 for s, g in ((st, gt), (sb, gb), (sc, gc)):
2613 if s[1] == 0.0 and g[1] != 0.0:
2614 svsum += 0.1*cm
2616 sh = max(shs, shsum)
2617 sv = max(svs, svsum)
2619 return sh, sv
2621 def get_grow(self):
2622 ghs, gvs = Widget.get_grow(self)
2623 gh = (self.left.get_grow()[0] +
2624 self.right.get_grow()[0] +
2625 self.center.get_grow()[0]) * ghs
2626 gv = (self.top.get_grow()[1] +
2627 self.bottom.get_grow()[1] +
2628 self.center.get_grow()[1]) * gvs
2629 return gh, gv
2631 def set_size(self, size, offset):
2632 (sh, sv), (oh, ov) = self.legalize(size, offset)
2634 sl, sr, st, sb, sc = [x.get_min_size() for x in (
2635 self.left, self.right, self.top, self.bottom, self.center)]
2636 gl, gr, gt, gb, gc = [x.get_grow() for x in (
2637 self.left, self.right, self.top, self.bottom, self.center)]
2639 ah = sh - (sl[0]+sr[0]+sc[0])
2640 av = sv - (st[1]+sb[1]+sc[1])
2642 if ah < 0.0:
2643 raise GmtPyError("Container not wide enough for contents "
2644 "(FrameLayout, available: %g cm, needed: %g cm)"
2645 % (sh/cm, (sl[0]+sr[0]+sc[0])/cm))
2646 if av < 0.0:
2647 raise GmtPyError("Container not high enough for contents "
2648 "(FrameLayout, available: %g cm, needed: %g cm)"
2649 % (sv/cm, (st[1]+sb[1]+sc[1])/cm))
2651 slh, srh, sch = distribute((sl[0], sr[0], sc[0]),
2652 (gl[0], gr[0], gc[0]), ah)
2653 stv, sbv, scv = distribute((st[1], sb[1], sc[1]),
2654 (gt[1], gb[1], gc[1]), av)
2656 if self.center.aspect is not None:
2657 ahm = sh - (sl[0]+sr[0] + scv/self.center.aspect)
2658 avm = sv - (st[1]+sb[1] + sch*self.center.aspect)
2659 if 0.0 < ahm < ah:
2660 slh, srh, sch = distribute(
2661 (sl[0], sr[0], scv/self.center.aspect),
2662 (gl[0], gr[0], 0.0), ahm)
2664 elif 0.0 < avm < av:
2665 stv, sbv, scv = distribute((st[1], sb[1],
2666 sch*self.center.aspect),
2667 (gt[1], gb[1], 0.0), avm)
2669 ah = sh - (slh+srh+sch)
2670 av = sv - (stv+sbv+scv)
2672 oh += ah/2.
2673 ov += av/2.
2674 sh -= ah
2675 sv -= av
2677 self.left.set_size((slh, scv), (oh, ov+sbv))
2678 self.right.set_size((srh, scv), (oh+slh+sch, ov+sbv))
2679 self.top.set_size((sh, stv), (oh, ov+sbv+scv))
2680 self.bottom.set_size((sh, sbv), (oh, ov))
2681 self.center.set_size((sch, scv), (oh+slh, ov+sbv))
2682 Widget.set_size(self, (sh, sv), (oh, ov))
2684 def set_widget(self, which='center', widget=None):
2686 '''
2687 Set one of the sub-widgets.
2689 ``which`` should be one of ``'left'``, ``'right'``, ``'top'``,
2690 ``'bottom'`` or ``'center'``.
2691 '''
2693 if widget is None:
2694 widget = Widget()
2696 if which in ('left', 'right', 'top', 'bottom', 'center'):
2697 self.__dict__[which] = widget
2698 else:
2699 raise GmtPyError('No such sub-widget: %s' % which)
2701 widget.set_parent(self)
2703 def get_widget(self, which='center'):
2705 '''
2706 Get one of the sub-widgets.
2708 ``which`` should be one of ``'left'``, ``'right'``, ``'top'``,
2709 ``'bottom'`` or ``'center'``.
2710 '''
2712 if which in ('left', 'right', 'top', 'bottom', 'center'):
2713 return self.__dict__[which]
2714 else:
2715 raise GmtPyError('No such sub-widget: %s' % which)
2717 def get_children(self):
2718 return [self.left, self.right, self.top, self.bottom, self.center]
2721class GridLayout(Widget):
2723 '''
2724 A layout manager which arranges its sub-widgets in a grid.
2726 The grid spacing is flexible and based on the sizing policies of the
2727 contained sub-widgets. If an equidistant grid is needed, the sizing
2728 policies of the sub-widgets have to be set equally.
2730 The height of each row and the width of each column is derived from the
2731 sizing policy of the largest sub-widget in the row or column in question.
2732 The algorithm is not very sophisticated, so conflicting sizing policies
2733 might not be resolved optimally.
2734 '''
2736 def __init__(self, nx=2, ny=2, horizontal=None, vertical=None):
2738 '''
2739 Create new grid layout with ``nx`` columns and ``ny`` rows.
2740 '''
2742 Widget.__init__(self, horizontal, vertical)
2743 self.grid = []
2744 for iy in range(ny):
2745 row = []
2746 for ix in range(nx):
2747 w = Widget(parent=self)
2748 row.append(w)
2750 self.grid.append(row)
2752 def sub_min_sizes_as_array(self):
2753 esh = num.array(
2754 [[w.get_min_size()[0] for w in row] for row in self.grid],
2755 dtype=float)
2756 esv = num.array(
2757 [[w.get_min_size()[1] for w in row] for row in self.grid],
2758 dtype=float)
2759 return esh, esv
2761 def sub_grows_as_array(self):
2762 egh = num.array(
2763 [[w.get_grow()[0] for w in row] for row in self.grid],
2764 dtype=float)
2765 egv = num.array(
2766 [[w.get_grow()[1] for w in row] for row in self.grid],
2767 dtype=float)
2768 return egh, egv
2770 def get_min_size(self):
2771 sh, sv = Widget.get_min_size(self)
2772 esh, esv = self.sub_min_sizes_as_array()
2773 if esh.size != 0:
2774 sh = max(sh, num.sum(esh.max(0)))
2775 if esv.size != 0:
2776 sv = max(sv, num.sum(esv.max(1)))
2777 return sh, sv
2779 def get_grow(self):
2780 ghs, gvs = Widget.get_grow(self)
2781 egh, egv = self.sub_grows_as_array()
2782 if egh.size != 0:
2783 gh = num.sum(egh.max(0))*ghs
2784 else:
2785 gh = 1.0
2786 if egv.size != 0:
2787 gv = num.sum(egv.max(1))*gvs
2788 else:
2789 gv = 1.0
2790 return gh, gv
2792 def set_size(self, size, offset):
2793 (sh, sv), (oh, ov) = self.legalize(size, offset)
2794 esh, esv = self.sub_min_sizes_as_array()
2795 egh, egv = self.sub_grows_as_array()
2797 # available additional space
2798 empty = esh.size == 0
2800 if not empty:
2801 ah = sh - num.sum(esh.max(0))
2802 av = sv - num.sum(esv.max(1))
2803 else:
2804 av = sv
2805 ah = sh
2807 if ah < 0.0:
2808 raise GmtPyError("Container not wide enough for contents "
2809 "(GridLayout, available: %g cm, needed: %g cm)"
2810 % (sh/cm, (num.sum(esh.max(0)))/cm))
2811 if av < 0.0:
2812 raise GmtPyError("Container not high enough for contents "
2813 "(GridLayout, available: %g cm, needed: %g cm)"
2814 % (sv/cm, (num.sum(esv.max(1)))/cm))
2816 nx, ny = esh.shape
2818 if not empty:
2819 # distribute additional space on rows and columns
2820 # according to grow weights and minimal sizes
2821 gsh = egh.sum(1)[:, num.newaxis].repeat(ny, axis=1)
2822 nesh = esh.copy()
2823 nesh += num.where(gsh > 0.0, ah*egh/gsh, 0.0)
2825 nsh = num.maximum(nesh.max(0), esh.max(0))
2827 gsv = egv.sum(0)[num.newaxis, :].repeat(nx, axis=0)
2828 nesv = esv.copy()
2829 nesv += num.where(gsv > 0.0, av*egv/gsv, 0.0)
2830 nsv = num.maximum(nesv.max(1), esv.max(1))
2832 ah = sh - sum(nsh)
2833 av = sv - sum(nsv)
2835 oh += ah/2.
2836 ov += av/2.
2837 sh -= ah
2838 sv -= av
2840 # resize child widgets
2841 neov = ov + sum(nsv)
2842 for row, nesv in zip(self.grid, nsv):
2843 neov -= nesv
2844 neoh = oh
2845 for w, nesh in zip(row, nsh):
2846 w.set_size((nesh, nesv), (neoh, neov))
2847 neoh += nesh
2849 Widget.set_size(self, (sh, sv), (oh, ov))
2851 def set_widget(self, ix, iy, widget=None):
2853 '''
2854 Set one of the sub-widgets.
2856 Sets the sub-widget in column ``ix`` and row ``iy``. The indices are
2857 counted from zero.
2858 '''
2860 if widget is None:
2861 widget = Widget()
2863 self.grid[iy][ix] = widget
2864 widget.set_parent(self)
2866 def get_widget(self, ix, iy):
2868 '''
2869 Get one of the sub-widgets.
2871 Gets the sub-widget from column ``ix`` and row ``iy``. The indices are
2872 counted from zero.
2873 '''
2875 return self.grid[iy][ix]
2877 def get_children(self):
2878 children = []
2879 for row in self.grid:
2880 children.extend(row)
2882 return children
2885def is_gmt5(version='newest'):
2886 return get_gmt_installation(version)['version'][0] in ['5', '6']
2889def is_gmt6(version='newest'):
2890 return get_gmt_installation(version)['version'][0] in ['6']
2893def aspect_for_projection(gmtversion, *args, **kwargs):
2895 gmt = GMT(version=gmtversion, eps_mode=True)
2897 if gmt.is_gmt5():
2898 gmt.psbasemap('-B+gblack', finish=True, *args, **kwargs)
2899 fn = gmt.tempfilename('test.eps')
2900 gmt.save(fn, crop_eps_mode=True)
2901 with open(fn, 'rb') as f:
2902 s = f.read()
2904 l, b, r, t = get_bbox(s)
2905 else:
2906 gmt.psbasemap('-G0', finish=True, *args, **kwargs)
2907 l, b, r, t = gmt.bbox()
2909 return (t-b)/(r-l)
2912def text_box(
2913 text, font=0, font_size=12., angle=0, gmtversion='newest', **kwargs):
2915 gmt = GMT(version=gmtversion)
2916 if gmt.is_gmt5():
2917 row = [0, 0, text]
2918 farg = ['-F+f%gp,%s,%s+j%s' % (font_size, font, 'black', 'BL')]
2919 else:
2920 row = [0, 0, font_size, 0, font, 'BL', text]
2921 farg = []
2923 gmt.pstext(
2924 in_rows=[row],
2925 finish=True,
2926 R=(0, 1, 0, 1),
2927 J='x10p',
2928 N=True,
2929 *farg,
2930 **kwargs)
2932 fn = gmt.tempfilename() + '.ps'
2933 gmt.save(fn)
2935 (_, stderr) = subprocess.Popen(
2936 ['gs', '-q', '-dNOPAUSE', '-dBATCH', '-r720', '-sDEVICE=bbox', fn],
2937 stderr=subprocess.PIPE).communicate()
2939 dx, dy = None, None
2940 for line in stderr.splitlines():
2941 if line.startswith(b'%%HiResBoundingBox:'):
2942 l, b, r, t = [float(x) for x in line.split()[-4:]]
2943 dx, dy = r-l, t-b
2944 break
2946 return dx, dy
2949class TableLiner(object):
2950 '''
2951 Utility class to turn tables into lines.
2952 '''
2954 def __init__(self, in_columns=None, in_rows=None, encoding='utf-8'):
2955 self.in_columns = in_columns
2956 self.in_rows = in_rows
2957 self.encoding = encoding
2959 def __iter__(self):
2960 if self.in_columns is not None:
2961 for row in zip(*self.in_columns):
2962 yield (' '.join([newstr(x) for x in row])+'\n').encode(
2963 self.encoding)
2965 if self.in_rows is not None:
2966 for row in self.in_rows:
2967 yield (' '.join([newstr(x) for x in row])+'\n').encode(
2968 self.encoding)
2971class LineStreamChopper(object):
2972 '''
2973 File-like object to buffer data.
2974 '''
2976 def __init__(self, liner):
2977 self.chopsize = None
2978 self.liner = liner
2979 self.chop_iterator = None
2980 self.closed = False
2982 def _chopiter(self):
2983 buf = BytesIO()
2984 for line in self.liner:
2985 buf.write(line)
2986 buflen = buf.tell()
2987 if self.chopsize is not None and buflen >= self.chopsize:
2988 buf.seek(0)
2989 while buf.tell() <= buflen-self.chopsize:
2990 yield buf.read(self.chopsize)
2992 newbuf = BytesIO()
2993 newbuf.write(buf.read())
2994 buf.close()
2995 buf = newbuf
2997 yield(buf.getvalue())
2998 buf.close()
3000 def read(self, size=None):
3001 if self.closed:
3002 raise ValueError('Cannot read from closed LineStreamChopper.')
3003 if self.chop_iterator is None:
3004 self.chopsize = size
3005 self.chop_iterator = self._chopiter()
3007 self.chopsize = size
3008 try:
3009 return next(self.chop_iterator)
3010 except StopIteration:
3011 return ''
3013 def close(self):
3014 self.chopsize = None
3015 self.chop_iterator = None
3016 self.closed = True
3018 def flush(self):
3019 pass
3022font_tab = {
3023 0: 'Helvetica',
3024 1: 'Helvetica-Bold',
3025}
3027font_tab_rev = dict((v, k) for (k, v) in font_tab.items())
3030class GMT(object):
3031 '''
3032 A thin wrapper to GMT command execution.
3034 A dict ``config`` may be given to override some of the default GMT
3035 parameters. The ``version`` argument may be used to select a specific GMT
3036 version, which should be used with this GMT instance. The selected
3037 version of GMT has to be installed on the system, must be supported by
3038 gmtpy and gmtpy must know where to find it.
3040 Each instance of this class is used for the task of producing one PS or PDF
3041 output file.
3043 Output of a series of GMT commands is accumulated in memory and can then be
3044 saved as PS or PDF file using the :py:meth:`save` method.
3046 GMT commands are accessed as method calls to instances of this class. See
3047 the :py:meth:`__getattr__` method for details on how the method's
3048 arguments are translated into options and arguments for the GMT command.
3050 Associated with each instance of this class, a temporary directory is
3051 created, where temporary files may be created, and which is automatically
3052 deleted, when the object is destroyed. The :py:meth:`tempfilename` method
3053 may be used to get a random filename in the instance's temporary directory.
3055 Any .gmtdefaults files are ignored. The GMT class uses a fixed
3056 set of defaults, which may be altered via an argument to the constructor.
3057 If possible, GMT is run in 'isolation mode', which was introduced with GMT
3058 version 4.2.2, by setting `GMT_TMPDIR` to the instance's temporary
3059 directory. With earlier versions of GMT, problems may arise with parallel
3060 execution of more than one GMT instance.
3062 Each instance of the GMT class may pick a specific version of GMT which
3063 shall be used, so that, if multiple versions of GMT are installed on the
3064 system, different versions of GMT can be used simultaneously such that
3065 backward compatibility of the scripts can be maintained.
3067 '''
3069 def __init__(
3070 self,
3071 config=None,
3072 kontinue=None,
3073 version='newest',
3074 config_papersize=None,
3075 eps_mode=False):
3077 self.installation = get_gmt_installation(version)
3078 self.gmt_config = dict(self.installation['defaults'])
3079 self.eps_mode = eps_mode
3080 self._shutil = shutil
3082 if config:
3083 self.gmt_config.update(config)
3085 if config_papersize:
3086 if not isinstance(config_papersize, str):
3087 config_papersize = 'Custom_%ix%i' % (
3088 int(config_papersize[0]), int(config_papersize[1]))
3090 if self.is_gmt5():
3091 self.gmt_config['PS_MEDIA'] = config_papersize
3092 else:
3093 self.gmt_config['PAPER_MEDIA'] = config_papersize
3095 self.tempdir = tempfile.mkdtemp("", "gmtpy-")
3096 self.gmt_config_filename = pjoin(self.tempdir, 'gmt.conf')
3097 self.gen_gmt_config_file(self.gmt_config_filename, self.gmt_config)
3099 if kontinue is not None:
3100 self.load_unfinished(kontinue)
3101 self.needstart = False
3102 else:
3103 self.output = BytesIO()
3104 self.needstart = True
3106 self.finished = False
3108 self.environ = os.environ.copy()
3109 self.environ['GMTHOME'] = self.installation.get('home', '')
3110 # GMT isolation mode: works only properly with GMT version >= 4.2.2
3111 self.environ['GMT_TMPDIR'] = self.tempdir
3113 self.layout = None
3114 self.command_log = []
3115 self.keep_temp_dir = False
3117 def is_gmt5(self):
3118 return self.get_version()[0] in ['5', '6']
3120 def is_gmt6(self):
3121 return self.get_version()[0] in ['6']
3123 def get_version(self):
3124 return self.installation['version']
3126 def get_config(self, key):
3127 return self.gmt_config[key]
3129 def to_points(self, string):
3130 if not string:
3131 return 0
3133 unit = string[-1]
3134 if unit in _units:
3135 return float(string[:-1])/_units[unit]
3136 else:
3137 default_unit = measure_unit(self.gmt_config).lower()[0]
3138 return float(string)/_units[default_unit]
3140 def label_font_size(self):
3141 if self.is_gmt5():
3142 return self.to_points(self.gmt_config['FONT_LABEL'].split(',')[0])
3143 else:
3144 return self.to_points(self.gmt_config['LABEL_FONT_SIZE'])
3146 def label_font(self):
3147 if self.is_gmt5():
3148 return font_tab_rev(self.gmt_config['FONT_LABEL'].split(',')[1])
3149 else:
3150 return self.gmt_config['LABEL_FONT']
3152 def gen_gmt_config_file(self, config_filename, config):
3153 f = open(config_filename, 'wb')
3154 f.write(
3155 ('#\n# GMT %s Defaults file\n'
3156 % self.installation['version']).encode('ascii'))
3158 for k, v in config.items():
3159 f.write(('%s = %s\n' % (k, v)).encode('ascii'))
3160 f.close()
3162 def __del__(self):
3163 if not self.keep_temp_dir:
3164 self._shutil.rmtree(self.tempdir)
3166 def _gmtcommand(self, command, *addargs, **kwargs):
3168 '''
3169 Execute arbitrary GMT command.
3171 See docstring in __getattr__ for details.
3172 '''
3174 in_stream = kwargs.pop('in_stream', None)
3175 in_filename = kwargs.pop('in_filename', None)
3176 in_string = kwargs.pop('in_string', None)
3177 in_columns = kwargs.pop('in_columns', None)
3178 in_rows = kwargs.pop('in_rows', None)
3179 out_stream = kwargs.pop('out_stream', None)
3180 out_filename = kwargs.pop('out_filename', None)
3181 out_discard = kwargs.pop('out_discard', None)
3182 finish = kwargs.pop('finish', False)
3183 suppressdefaults = kwargs.pop('suppress_defaults', False)
3184 config_override = kwargs.pop('config', None)
3186 assert(not self.finished)
3188 # check for mutual exclusiveness on input and output possibilities
3189 assert(1 >= len(
3190 [x for x in [
3191 in_stream, in_filename, in_string, in_columns, in_rows]
3192 if x is not None]))
3193 assert(1 >= len([x for x in [out_stream, out_filename, out_discard]
3194 if x is not None]))
3196 options = []
3198 gmt_config = self.gmt_config
3199 if not self.is_gmt5():
3200 gmt_config_filename = self.gmt_config_filename
3201 if config_override:
3202 gmt_config = self.gmt_config.copy()
3203 gmt_config.update(config_override)
3204 gmt_config_override_filename = pjoin(
3205 self.tempdir, 'gmtdefaults_override')
3206 self.gen_gmt_config_file(
3207 gmt_config_override_filename, gmt_config)
3208 gmt_config_filename = gmt_config_override_filename
3210 else: # gmt5 needs override variables as --VAR=value
3211 if config_override:
3212 for k, v in config_override.items():
3213 options.append('--%s=%s' % (k, v))
3215 if out_discard:
3216 out_filename = '/dev/null'
3218 out_mustclose = False
3219 if out_filename is not None:
3220 out_mustclose = True
3221 out_stream = open(out_filename, 'wb')
3223 if in_filename is not None:
3224 in_stream = open(in_filename, 'rb')
3226 if in_string is not None:
3227 in_stream = BytesIO(in_string)
3229 encoding_gmt = gmt_config.get(
3230 'PS_CHAR_ENCODING',
3231 gmt_config.get('CHAR_ENCODING', 'ISOLatin1+'))
3233 encoding = encoding_gmt_to_python[encoding_gmt.lower()]
3235 if in_columns is not None or in_rows is not None:
3236 in_stream = LineStreamChopper(TableLiner(in_columns=in_columns,
3237 in_rows=in_rows,
3238 encoding=encoding))
3240 # convert option arguments to strings
3241 for k, v in kwargs.items():
3242 if len(k) > 1:
3243 raise GmtPyError('Found illegal keyword argument "%s" '
3244 'while preparing options for command "%s"'
3245 % (k, command))
3247 if type(v) is bool:
3248 if v:
3249 options.append('-%s' % k)
3250 elif type(v) is tuple or type(v) is list:
3251 options.append('-%s' % k + '/'.join([str(x) for x in v]))
3252 else:
3253 options.append('-%s%s' % (k, str(v)))
3255 # if not redirecting to an external sink, handle -K -O
3256 if out_stream is None:
3257 if not finish:
3258 options.append('-K')
3259 else:
3260 self.finished = True
3262 if not self.needstart:
3263 options.append('-O')
3264 else:
3265 self.needstart = False
3267 out_stream = self.output
3269 # run the command
3270 if self.is_gmt5():
3271 args = [pjoin(self.installation['bin'], 'gmt'), command]
3272 else:
3273 args = [pjoin(self.installation['bin'], command)]
3275 if not os.path.isfile(args[0]):
3276 raise OSError('No such file: %s' % args[0])
3277 args.extend(options)
3278 args.extend(addargs)
3279 if not self.is_gmt5() and not suppressdefaults:
3280 # does not seem to work with GMT 5 (and should not be necessary
3281 args.append('+'+gmt_config_filename)
3283 bs = 2048
3284 p = subprocess.Popen(args, stdin=subprocess.PIPE,
3285 stdout=subprocess.PIPE, bufsize=bs,
3286 env=self.environ)
3287 while True:
3288 cr, cw, cx = select([p.stdout], [p.stdin], [])
3289 if cr:
3290 out_stream.write(p.stdout.read(bs))
3291 if cw:
3292 if in_stream is not None:
3293 data = in_stream.read(bs)
3294 if len(data) == 0:
3295 break
3296 p.stdin.write(data)
3297 else:
3298 break
3299 if not cr and not cw:
3300 break
3302 p.stdin.close()
3304 while True:
3305 data = p.stdout.read(bs)
3306 if len(data) == 0:
3307 break
3308 out_stream.write(data)
3310 p.stdout.close()
3312 retcode = p.wait()
3314 if in_stream is not None:
3315 in_stream.close()
3317 if out_mustclose:
3318 out_stream.close()
3320 if retcode != 0:
3321 self.keep_temp_dir = True
3322 raise GMTError('Command %s returned an error. '
3323 'While executing command:\n%s'
3324 % (command, escape_shell_args(args)))
3326 self.command_log.append(args)
3328 def __getattr__(self, command):
3330 '''
3331 Maps to call self._gmtcommand(command, \\*addargs, \\*\\*kwargs).
3333 Execute arbitrary GMT command.
3335 Run a GMT command and by default append its postscript output to the
3336 output file maintained by the GMT instance on which this method is
3337 called.
3339 Except for a few keyword arguments listed below, any ``kwargs`` and
3340 ``addargs`` are converted into command line options and arguments and
3341 passed to the GMT command. Numbers in keyword arguments are converted
3342 into strings. E.g. ``S=10`` is translated into ``'-S10'``. Tuples of
3343 numbers or strings are converted into strings where the elements of the
3344 tuples are separated by slashes '/'. E.g. ``R=(10, 10, 20, 20)`` is
3345 translated into ``'-R10/10/20/20'``. Options with a boolean argument
3346 are only appended to the GMT command, if their values are True.
3348 If no output redirection is in effect, the -K and -O options are
3349 handled by gmtpy and thus should not be specified. Use
3350 ``out_discard=True`` if you don't want -K or -O beeing added, but are
3351 not interested in the output.
3353 The standard input of the GMT process is fed by data selected with one
3354 of the following ``in_*`` keyword arguments:
3356 =============== =======================================================
3357 ``in_stream`` Data is read from an open file like object.
3358 ``in_filename`` Data is read from the given file.
3359 ``in_string`` String content is dumped to the process.
3360 ``in_columns`` A 2D nested iterable whose elements can be accessed as
3361 ``in_columns[icolumn][irow]`` is converted into an
3362 ascii
3363 table, which is fed to the process.
3364 ``in_rows`` A 2D nested iterable whos elements can be accessed as
3365 ``in_rows[irow][icolumn]`` is converted into an ascii
3366 table, which is fed to the process.
3367 =============== =======================================================
3369 The standard output of the GMT process may be redirected by one of the
3370 following options:
3372 ================= =====================================================
3373 ``out_stream`` Output is fed to an open file like object.
3374 ``out_filename`` Output is dumped to the given file.
3375 ``out_discard`` If True, output is dumped to :file:`/dev/null`.
3376 ================= =====================================================
3378 Additional keyword arguments:
3380 ===================== =================================================
3381 ``config`` Dict with GMT defaults which override the
3382 currently active set of defaults exclusively
3383 during this call.
3384 ``finish`` If True, the postscript file, which is maintained
3385 by the GMT instance is finished, and no further
3386 plotting is allowed.
3387 ``suppress_defaults`` Suppress appending of the ``'+gmtdefaults'``
3388 option to the command.
3389 ===================== =================================================
3391 '''
3393 def f(*args, **kwargs):
3394 return self._gmtcommand(command, *args, **kwargs)
3395 return f
3397 def tempfilename(self, name=None):
3398 '''
3399 Get filename for temporary file in the private temp directory.
3401 If no ``name`` argument is given, a random name is picked. If
3402 ``name`` is given, returns a path ending in that ``name``.
3403 '''
3405 if not name:
3406 name = ''.join(
3407 [random.choice('abcdefghijklmnopqrstuvwxyz')
3408 for i in range(10)])
3410 fn = pjoin(self.tempdir, name)
3411 return fn
3413 def tempfile(self, name=None):
3414 '''
3415 Create and open a file in the private temp directory.
3416 '''
3418 fn = self.tempfilename(name)
3419 f = open(fn, 'wb')
3420 return f, fn
3422 def save_unfinished(self, filename):
3423 out = open(filename, 'wb')
3424 out.write(self.output.getvalue())
3425 out.close()
3427 def load_unfinished(self, filename):
3428 self.output = BytesIO()
3429 self.finished = False
3430 inp = open(filename, 'rb')
3431 self.output.write(inp.read())
3432 inp.close()
3434 def dump(self, ident):
3435 filename = self.tempfilename('breakpoint-%s' % ident)
3436 self.save_unfinished(filename)
3438 def load(self, ident):
3439 filename = self.tempfilename('breakpoint-%s' % ident)
3440 self.load_unfinished(filename)
3442 def save(self, filename=None, bbox=None, resolution=150, oversample=2.,
3443 width=None, height=None, size=None, crop_eps_mode=False,
3444 psconvert=False):
3446 '''
3447 Finish and save figure as PDF, PS or PPM file.
3449 If filename ends with ``'.pdf'`` a PDF file is created by piping the
3450 GMT output through :program:`gmtpy-epstopdf`.
3452 If filename ends with ``'.png'`` a PNG file is created by running
3453 :program:`gmtpy-epstopdf`, :program:`pdftocairo` and
3454 :program:`convert`. ``resolution`` specifies the resolution in DPI for
3455 raster file formats. Rasterization is done at a higher resolution if
3456 ``oversample`` is set to a value higher than one. The output image size
3457 can also be controlled by setting ``width``, ``height`` or ``size``
3458 instead of ``resolution``. When ``size`` is given, the image is scaled
3459 so that ``max(width, height) == size``.
3461 The bounding box is set according to the values given in ``bbox``.
3462 '''
3464 if not self.finished:
3465 self.psxy(R=True, J=True, finish=True)
3467 if filename:
3468 tempfn = pjoin(self.tempdir, 'incomplete')
3469 out = open(tempfn, 'wb')
3470 else:
3471 out = sys.stdout
3473 if bbox and not self.is_gmt5():
3474 out.write(replace_bbox(bbox, self.output.getvalue()))
3475 else:
3476 out.write(self.output.getvalue())
3478 if filename:
3479 out.close()
3481 if filename.endswith('.ps') or (
3482 not self.is_gmt5() and filename.endswith('.eps')):
3484 shutil.move(tempfn, filename)
3485 return
3487 if self.is_gmt5():
3488 if crop_eps_mode:
3489 addarg = ['-A']
3490 else:
3491 addarg = []
3493 subprocess.call(
3494 [pjoin(self.installation['bin'], 'gmt'), 'psconvert',
3495 '-Te', '-F%s' % tempfn, tempfn, ] + addarg)
3497 if bbox:
3498 with open(tempfn + '.eps', 'rb') as fin:
3499 with open(tempfn + '-fixbb.eps', 'wb') as fout:
3500 replace_bbox(bbox, fin, fout)
3502 shutil.move(tempfn + '-fixbb.eps', tempfn + '.eps')
3504 else:
3505 shutil.move(tempfn, tempfn + '.eps')
3507 if filename.endswith('.eps'):
3508 shutil.move(tempfn + '.eps', filename)
3509 return
3511 elif filename.endswith('.pdf'):
3512 if psconvert:
3513 gmt_bin = pjoin(self.installation['bin'], 'gmt')
3514 subprocess.call([gmt_bin, 'psconvert', tempfn + '.eps', '-Tf',
3515 '-F' + filename])
3516 else:
3517 subprocess.call(['gmtpy-epstopdf', '--res=%i' % resolution,
3518 '--outfile=' + filename, tempfn + '.eps'])
3519 else:
3520 subprocess.call([
3521 'gmtpy-epstopdf',
3522 '--res=%i' % (resolution * oversample),
3523 '--outfile=' + tempfn + '.pdf', tempfn + '.eps'])
3525 convert_graph(
3526 tempfn + '.pdf', filename,
3527 resolution=resolution, oversample=oversample,
3528 size=size, width=width, height=height)
3530 def bbox(self):
3531 return get_bbox(self.output.getvalue())
3533 def get_command_log(self):
3534 '''
3535 Get the command log.
3536 '''
3538 return self.command_log
3540 def __str__(self):
3541 s = ''
3542 for com in self.command_log:
3543 s += com[0] + "\n " + "\n ".join(com[1:]) + "\n\n"
3544 return s
3546 def page_size_points(self):
3547 '''
3548 Try to get paper size of output postscript file in points.
3549 '''
3551 pm = paper_media(self.gmt_config).lower()
3552 if pm.endswith('+') or pm.endswith('-'):
3553 pm = pm[:-1]
3555 orient = page_orientation(self.gmt_config).lower()
3557 if pm in all_paper_sizes():
3559 if orient == 'portrait':
3560 return get_paper_size(pm)
3561 else:
3562 return get_paper_size(pm)[1], get_paper_size(pm)[0]
3564 m = re.match(r'custom_([0-9.]+)([cimp]?)x([0-9.]+)([cimp]?)', pm)
3565 if m:
3566 w, uw, h, uh = m.groups()
3567 w, h = float(w), float(h)
3568 if uw:
3569 w *= _units[uw]
3570 if uh:
3571 h *= _units[uh]
3572 if orient == 'portrait':
3573 return w, h
3574 else:
3575 return h, w
3577 return None, None
3579 def default_layout(self, with_palette=False):
3580 '''
3581 Get a default layout for the output page.
3583 One of three different layouts is choosen, depending on the
3584 `PAPER_MEDIA` setting in the GMT configuration dict.
3586 If `PAPER_MEDIA` ends with a ``'+'`` (EPS output is selected), a
3587 :py:class:`FrameLayout` is centered on the page, whose size is
3588 controlled by its center widget's size plus the margins of the
3589 :py:class:`FrameLayout`.
3591 If `PAPER_MEDIA` indicates, that a custom page size is wanted by
3592 starting with ``'Custom_'``, a :py:class:`FrameLayout` is used to fill
3593 the complete page. The center widget's size is then controlled by the
3594 page's size minus the margins of the :py:class:`FrameLayout`.
3596 In any other case, two FrameLayouts are nested, such that the outer
3597 layout attaches a 1 cm (printer) margin around the complete page, and
3598 the inner FrameLayout's center widget takes up as much space as
3599 possible under the constraint, that an aspect ratio of 1/golden_ratio
3600 is preserved.
3602 In any case, a reference to the innermost :py:class:`FrameLayout`
3603 instance is returned. The top-level layout can be accessed by calling
3604 :py:meth:`Widget.get_parent` on the returned layout.
3605 '''
3607 if self.layout is None:
3608 w, h = self.page_size_points()
3610 if w is None or h is None:
3611 raise GmtPyError("Can't determine page size for layout")
3613 pm = paper_media(self.gmt_config).lower()
3615 if with_palette:
3616 palette_layout = GridLayout(3, 1)
3617 spacer = palette_layout.get_widget(1, 0)
3618 palette_widget = palette_layout.get_widget(2, 0)
3619 spacer.set_horizontal(0.5*cm)
3620 palette_widget.set_horizontal(0.5*cm)
3622 if pm.endswith('+') or self.eps_mode:
3623 outer = CenterLayout()
3624 outer.set_policy((w, h), (0., 0.))
3625 inner = FrameLayout()
3626 outer.set_widget(inner)
3627 if with_palette:
3628 inner.set_widget('center', palette_layout)
3629 widget = palette_layout
3630 else:
3631 widget = inner.get_widget('center')
3632 widget.set_policy((w/golden_ratio, 0.), (0., 0.),
3633 aspect=1./golden_ratio)
3634 mw = 3.0*cm
3635 inner.set_fixed_margins(
3636 mw, mw, mw/golden_ratio, mw/golden_ratio)
3637 self.layout = inner
3639 elif pm.startswith('custom_'):
3640 layout = FrameLayout()
3641 layout.set_policy((w, h), (0., 0.))
3642 mw = 3.0*cm
3643 layout.set_min_margins(
3644 mw, mw, mw/golden_ratio, mw/golden_ratio)
3645 if with_palette:
3646 layout.set_widget('center', palette_layout)
3647 self.layout = layout
3648 else:
3649 outer = FrameLayout()
3650 outer.set_policy((w, h), (0., 0.))
3651 outer.set_fixed_margins(1.*cm, 1.*cm, 1.*cm, 1.*cm)
3653 inner = FrameLayout()
3654 outer.set_widget('center', inner)
3655 mw = 3.0*cm
3656 inner.set_min_margins(mw, mw, mw/golden_ratio, mw/golden_ratio)
3657 if with_palette:
3658 inner.set_widget('center', palette_layout)
3659 widget = palette_layout
3660 else:
3661 widget = inner.get_widget('center')
3663 widget.set_aspect(1./golden_ratio)
3665 self.layout = inner
3667 return self.layout
3669 def draw_layout(self, layout):
3670 '''
3671 Use psxy to draw layout; for debugging
3672 '''
3674 # corners = layout.get_corners(descend=True)
3675 rects = num.array(layout.get_sizes(), dtype=float)
3676 rects_wid = rects[:, 0, 0]
3677 rects_hei = rects[:, 0, 1]
3678 rects_center_x = rects[:, 1, 0] + rects_wid*0.5
3679 rects_center_y = rects[:, 1, 1] + rects_hei*0.5
3680 nrects = len(rects)
3681 prects = (rects_center_x, rects_center_y, num.arange(nrects),
3682 num.zeros(nrects), rects_hei, rects_wid)
3684 # points = num.array(corners, dtype=float)
3686 cptfile = self.tempfilename() + '.cpt'
3687 self.makecpt(
3688 C='ocean',
3689 T='%g/%g/%g' % (-nrects, nrects, 1),
3690 Z=True,
3691 out_filename=cptfile, suppress_defaults=True)
3693 bb = layout.bbox()
3694 self.psxy(
3695 in_columns=prects,
3696 C=cptfile,
3697 W='1p',
3698 S='J',
3699 R=(bb[0], bb[2], bb[1], bb[3]),
3700 *layout.XYJ())
3703def simpleconf_to_ax(conf, axname):
3704 c = {}
3705 x = axname
3706 for x in ('', axname):
3707 for k in ('label', 'unit', 'scaled_unit', 'scaled_unit_factor',
3708 'space', 'mode', 'approx_ticks', 'limits', 'masking', 'inc',
3709 'snap'):
3711 if x+k in conf:
3712 c[k] = conf[x+k]
3714 return Ax(**c)
3717class DensityPlotDef(object):
3718 def __init__(self, data, cpt='ocean', tension=0.7, size=(640, 480),
3719 contour=False, method='surface', zscaler=None, **extra):
3720 self.data = data
3721 self.cpt = cpt
3722 self.tension = tension
3723 self.size = size
3724 self.contour = contour
3725 self.method = method
3726 self.zscaler = zscaler
3727 self.extra = extra
3730class TextDef(object):
3731 def __init__(
3732 self,
3733 data,
3734 size=9,
3735 justify='MC',
3736 fontno=0,
3737 offset=(0, 0),
3738 color='black'):
3740 self.data = data
3741 self.size = size
3742 self.justify = justify
3743 self.fontno = fontno
3744 self.offset = offset
3745 self.color = color
3748class Simple(object):
3749 def __init__(self, gmtconfig=None, gmtversion='newest', **simple_config):
3750 self.data = []
3751 self.symbols = []
3752 self.config = copy.deepcopy(simple_config)
3753 self.gmtconfig = gmtconfig
3754 self.density_plot_defs = []
3755 self.text_defs = []
3757 self.gmtversion = gmtversion
3759 self.data_x = []
3760 self.symbols_x = []
3762 self.data_y = []
3763 self.symbols_y = []
3765 self.default_config = {}
3766 self.set_defaults(width=15.*cm,
3767 height=15.*cm / golden_ratio,
3768 margins=(2.*cm, 2.*cm, 2.*cm, 2.*cm),
3769 with_palette=False,
3770 palette_offset=0.5*cm,
3771 palette_width=None,
3772 palette_height=None,
3773 zlabeloffset=2*cm,
3774 draw_layout=False)
3776 self.setup_defaults()
3777 self.fixate_widget_aspect = False
3779 def setup_defaults(self):
3780 pass
3782 def set_defaults(self, **kwargs):
3783 self.default_config.update(kwargs)
3785 def plot(self, data, symbol=''):
3786 self.data.append(data)
3787 self.symbols.append(symbol)
3789 def density_plot(self, data, **kwargs):
3790 dpd = DensityPlotDef(data, **kwargs)
3791 self.density_plot_defs.append(dpd)
3793 def text(self, data, **kwargs):
3794 dpd = TextDef(data, **kwargs)
3795 self.text_defs.append(dpd)
3797 def plot_x(self, data, symbol=''):
3798 self.data_x.append(data)
3799 self.symbols_x.append(symbol)
3801 def plot_y(self, data, symbol=''):
3802 self.data_y.append(data)
3803 self.symbols_y.append(symbol)
3805 def set(self, **kwargs):
3806 self.config.update(kwargs)
3808 def setup_base(self, conf):
3809 w = conf.pop('width')
3810 h = conf.pop('height')
3811 margins = conf.pop('margins')
3813 gmtconfig = {}
3814 if self.gmtconfig is not None:
3815 gmtconfig.update(self.gmtconfig)
3817 gmt = GMT(
3818 version=self.gmtversion,
3819 config=gmtconfig,
3820 config_papersize='Custom_%ix%i' % (w, h))
3822 layout = gmt.default_layout(with_palette=conf['with_palette'])
3823 layout.set_min_margins(*margins)
3824 if conf['with_palette']:
3825 widget = layout.get_widget().get_widget(0, 0)
3826 spacer = layout.get_widget().get_widget(1, 0)
3827 spacer.set_horizontal(conf['palette_offset'])
3828 palette_widget = layout.get_widget().get_widget(2, 0)
3829 if conf['palette_width'] is not None:
3830 palette_widget.set_horizontal(conf['palette_width'])
3831 if conf['palette_height'] is not None:
3832 palette_widget.set_vertical(conf['palette_height'])
3833 widget.set_vertical(h-margins[2]-margins[3]-0.03*cm)
3834 return gmt, layout, widget, palette_widget
3835 else:
3836 widget = layout.get_widget()
3837 return gmt, layout, widget, None
3839 def setup_projection(self, widget, scaler, conf):
3840 pass
3842 def setup_scaling(self, conf):
3843 ndims = 2
3844 if self.density_plot_defs:
3845 ndims = 3
3847 axes = [simpleconf_to_ax(conf, x) for x in 'xyz'[:ndims]]
3849 data_all = []
3850 data_all.extend(self.data)
3851 for dsd in self.density_plot_defs:
3852 if dsd.zscaler is None:
3853 data_all.append(dsd.data)
3854 else:
3855 data_all.append(dsd.data[:2])
3856 data_chopped = [ds[:ndims] for ds in data_all]
3858 scaler = ScaleGuru(data_chopped, axes=axes[:ndims])
3860 self.setup_scaling_plus(scaler, axes[:ndims])
3862 return scaler
3864 def setup_scaling_plus(self, scaler, axes):
3865 pass
3867 def setup_scaling_extra(self, scaler, conf):
3869 scaler_x = scaler.copy()
3870 scaler_x.data_ranges[1] = (0., 1.)
3871 scaler_x.axes[1].mode = 'off'
3873 scaler_y = scaler.copy()
3874 scaler_y.data_ranges[0] = (0., 1.)
3875 scaler_y.axes[0].mode = 'off'
3877 return scaler_x, scaler_y
3879 def draw_density(self, gmt, widget, scaler):
3881 R = scaler.R()
3882 # par = scaler.get_params()
3883 rxyj = R + widget.XYJ()
3884 innerticks = False
3885 for dpd in self.density_plot_defs:
3887 fn_cpt = gmt.tempfilename() + '.cpt'
3889 if dpd.zscaler is not None:
3890 s = dpd.zscaler
3891 else:
3892 s = scaler
3894 gmt.makecpt(C=dpd.cpt, out_filename=fn_cpt, *s.T())
3896 fn_grid = gmt.tempfilename()
3898 fn_mean = gmt.tempfilename()
3900 if dpd.method in ('surface', 'triangulate'):
3901 gmt.blockmean(in_columns=dpd.data,
3902 I='%i+/%i+' % dpd.size, # noqa
3903 out_filename=fn_mean, *R)
3905 if dpd.method == 'surface':
3906 gmt.surface(
3907 in_filename=fn_mean,
3908 T=dpd.tension,
3909 G=fn_grid,
3910 I='%i+/%i+' % dpd.size, # noqa
3911 out_discard=True,
3912 *R)
3914 if dpd.method == 'triangulate':
3915 gmt.triangulate(
3916 in_filename=fn_mean,
3917 G=fn_grid,
3918 I='%i+/%i+' % dpd.size, # noqa
3919 out_discard=True,
3920 V=True,
3921 *R)
3923 if gmt.is_gmt5():
3924 gmt.grdimage(fn_grid, C=fn_cpt, E='i', n='l', *rxyj)
3926 else:
3927 gmt.grdimage(fn_grid, C=fn_cpt, E='i', S='l', *rxyj)
3929 if dpd.contour:
3930 gmt.grdcontour(fn_grid, C=fn_cpt, W='0.5p,black', *rxyj)
3931 innerticks = '0.5p,black'
3933 os.remove(fn_grid)
3934 os.remove(fn_mean)
3936 if dpd.method == 'fillcontour':
3937 extra = dict(C=fn_cpt)
3938 extra.update(dpd.extra)
3939 gmt.pscontour(in_columns=dpd.data,
3940 I=True, *rxyj, **extra) # noqa
3942 if dpd.method == 'contour':
3943 extra = dict(W='0.5p,black', C=fn_cpt)
3944 extra.update(dpd.extra)
3945 gmt.pscontour(in_columns=dpd.data, *rxyj, **extra)
3947 return fn_cpt, innerticks
3949 def draw_basemap(self, gmt, widget, scaler):
3950 gmt.psbasemap(*(widget.JXY() + scaler.RB(ax_projection=True)))
3952 def draw(self, gmt, widget, scaler):
3953 rxyj = scaler.R() + widget.JXY()
3954 for dat, sym in zip(self.data, self.symbols):
3955 gmt.psxy(in_columns=dat, *(sym.split()+rxyj))
3957 def post_draw(self, gmt, widget, scaler):
3958 pass
3960 def pre_draw(self, gmt, widget, scaler):
3961 pass
3963 def draw_extra(self, gmt, widget, scaler_x, scaler_y):
3965 for dat, sym in zip(self.data_x, self.symbols_x):
3966 gmt.psxy(in_columns=dat,
3967 *(sym.split() + scaler_x.R() + widget.JXY()))
3969 for dat, sym in zip(self.data_y, self.symbols_y):
3970 gmt.psxy(in_columns=dat,
3971 *(sym.split() + scaler_y.R() + widget.JXY()))
3973 def draw_text(self, gmt, widget, scaler):
3975 rxyj = scaler.R() + widget.JXY()
3976 for td in self.text_defs:
3977 x, y = td.data[0:2]
3978 text = td.data[-1]
3979 size = td.size
3980 angle = 0
3981 fontno = td.fontno
3982 justify = td.justify
3983 color = td.color
3984 if gmt.is_gmt5():
3985 gmt.pstext(
3986 in_rows=[(x, y, text)],
3987 F='+f%gp,%s,%s+a%g+j%s' % (
3988 size, fontno, color, angle, justify),
3989 D='%gp/%gp' % td.offset, *rxyj)
3990 else:
3991 gmt.pstext(
3992 in_rows=[(x, y, size, angle, fontno, justify, text)],
3993 D='%gp/%gp' % td.offset, *rxyj)
3995 def save(self, filename, resolution=150):
3997 conf = dict(self.default_config)
3998 conf.update(self.config)
4000 gmt, layout, widget, palette_widget = self.setup_base(conf)
4001 scaler = self.setup_scaling(conf)
4002 scaler_x, scaler_y = self.setup_scaling_extra(scaler, conf)
4004 self.setup_projection(widget, scaler, conf)
4005 if self.fixate_widget_aspect:
4006 aspect = aspect_for_projection(
4007 gmt.installation['version'], *(widget.J() + scaler.R()))
4009 widget.set_aspect(aspect)
4011 if conf['draw_layout']:
4012 gmt.draw_layout(layout)
4013 cptfile = None
4014 if self.density_plot_defs:
4015 cptfile, innerticks = self.draw_density(gmt, widget, scaler)
4016 self.pre_draw(gmt, widget, scaler)
4017 self.draw(gmt, widget, scaler)
4018 self.post_draw(gmt, widget, scaler)
4019 self.draw_extra(gmt, widget, scaler_x, scaler_y)
4020 self.draw_text(gmt, widget, scaler)
4021 self.draw_basemap(gmt, widget, scaler)
4023 if palette_widget and cptfile:
4024 nice_palette(gmt, palette_widget, scaler, cptfile,
4025 innerticks=innerticks,
4026 zlabeloffset=conf['zlabeloffset'])
4028 gmt.save(filename, resolution=resolution)
4031class LinLinPlot(Simple):
4032 pass
4035class LogLinPlot(Simple):
4037 def setup_defaults(self):
4038 self.set_defaults(xmode='min-max')
4040 def setup_projection(self, widget, scaler, conf):
4041 widget['J'] = '-JX%(width)gpl/%(height)gp'
4042 scaler['B'] = '-B2:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen'
4045class LinLogPlot(Simple):
4047 def setup_defaults(self):
4048 self.set_defaults(ymode='min-max')
4050 def setup_projection(self, widget, scaler, conf):
4051 widget['J'] = '-JX%(width)gp/%(height)gpl'
4052 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/2:%(ylabel)s:WSen'
4055class LogLogPlot(Simple):
4057 def setup_defaults(self):
4058 self.set_defaults(mode='min-max')
4060 def setup_projection(self, widget, scaler, conf):
4061 widget['J'] = '-JX%(width)gpl/%(height)gpl'
4062 scaler['B'] = '-B2:%(xlabel)s:/2:%(ylabel)s:WSen'
4065class AziDistPlot(Simple):
4067 def __init__(self, *args, **kwargs):
4068 Simple.__init__(self, *args, **kwargs)
4069 self.fixate_widget_aspect = True
4071 def setup_defaults(self):
4072 self.set_defaults(
4073 height=15.*cm,
4074 width=15.*cm,
4075 xmode='off',
4076 xlimits=(0., 360.),
4077 xinc=45.)
4079 def setup_projection(self, widget, scaler, conf):
4080 widget['J'] = '-JPa%(width)gp'
4082 def setup_scaling_plus(self, scaler, axes):
4083 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/%(yinc)g:%(ylabel)s:N'
4086class MPlot(Simple):
4088 def __init__(self, *args, **kwargs):
4089 Simple.__init__(self, *args, **kwargs)
4090 self.fixate_widget_aspect = True
4092 def setup_defaults(self):
4093 self.set_defaults(xmode='min-max', ymode='min-max')
4095 def setup_projection(self, widget, scaler, conf):
4096 par = scaler.get_params()
4097 lon0 = (par['xmin'] + par['xmax'])/2.
4098 lat0 = (par['ymin'] + par['ymax'])/2.
4099 sll = '%g/%g' % (lon0, lat0)
4100 widget['J'] = '-JM' + sll + '/%(width)gp'
4101 scaler['B'] = \
4102 '-B%(xinc)gg%(xinc)g:%(xlabel)s:/%(yinc)gg%(yinc)g:%(ylabel)s:WSen'
4105def nice_palette(gmt, widget, scaleguru, cptfile, zlabeloffset=0.8*inch,
4106 innerticks=True):
4108 par = scaleguru.get_params()
4109 par_ax = scaleguru.get_params(ax_projection=True)
4110 nz_palette = int(widget.height()/inch * 300)
4111 px = num.zeros(nz_palette*2)
4112 px[1::2] += 1
4113 pz = num.linspace(par['zmin'], par['zmax'], nz_palette).repeat(2)
4114 pdz = pz[2]-pz[0]
4115 palgrdfile = gmt.tempfilename()
4116 pal_r = (0, 1, par['zmin'], par['zmax'])
4117 pal_ax_r = (0, 1, par_ax['zmin'], par_ax['zmax'])
4118 gmt.xyz2grd(
4119 G=palgrdfile, R=pal_r,
4120 I=(1, pdz), in_columns=(px, pz, pz), # noqa
4121 out_discard=True)
4123 gmt.grdimage(palgrdfile, R=pal_r, C=cptfile, *widget.JXY())
4124 if isinstance(innerticks, str):
4125 tickpen = innerticks
4126 gmt.grdcontour(palgrdfile, W=tickpen, R=pal_r, C=cptfile,
4127 *widget.JXY())
4129 negpalwid = '%gp' % -widget.width()
4130 if not isinstance(innerticks, str) and innerticks:
4131 ticklen = negpalwid
4132 else:
4133 ticklen = '0p'
4135 TICK_LENGTH_PARAM = 'MAP_TICK_LENGTH' if gmt.is_gmt5() else 'TICK_LENGTH'
4136 gmt.psbasemap(
4137 R=pal_ax_r, B='4::/%(zinc)g::nsw' % par_ax,
4138 config={TICK_LENGTH_PARAM: ticklen},
4139 *widget.JXY())
4141 if innerticks:
4142 gmt.psbasemap(
4143 R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax,
4144 config={TICK_LENGTH_PARAM: '0p'},
4145 *widget.JXY())
4146 else:
4147 gmt.psbasemap(R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax, *widget.JXY())
4149 if par_ax['zlabel']:
4150 label_font = gmt.label_font()
4151 label_font_size = gmt.label_font_size()
4152 label_offset = zlabeloffset
4153 gmt.pstext(
4154 R=(0, 1, 0, 2), D="%gp/0p" % label_offset,
4155 N=True,
4156 in_rows=[(1, 1, label_font_size, -90, label_font, 'CB',
4157 par_ax['zlabel'])],
4158 *widget.JXY())