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
34try:
35 newstr = unicode
36except NameError:
37 newstr = str
39find_bb = re.compile(br'%%BoundingBox:((\s+[-0-9]+){4})')
40find_hiresbb = re.compile(br'%%HiResBoundingBox:((\s+[-0-9.]+){4})')
43encoding_gmt_to_python = {
44 'isolatin1+': 'iso-8859-1',
45 'standard+': 'ascii',
46 'isolatin1': 'iso-8859-1',
47 'standard': 'ascii'}
49for i in range(1, 11):
50 encoding_gmt_to_python['iso-8859-%i' % i] = 'iso-8859-%i' % i
53def have_gmt():
54 try:
55 get_gmt_installation('newest')
56 return True
58 except GMTInstallationProblem:
59 return False
62def check_have_gmt():
63 if not have_gmt():
64 raise ExternalProgramMissing('GMT is not installed or cannot be found')
67def have_pixmaptools():
68 for prog in [['pdftocairo'], ['convert'], ['gs', '-h']]:
69 try:
70 p = subprocess.Popen(
71 prog,
72 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
74 (stdout, stderr) = p.communicate()
76 except OSError:
77 return False
79 return True
82class GmtPyError(Exception):
83 pass
86class GMTError(GmtPyError):
87 pass
90class GMTInstallationProblem(GmtPyError):
91 pass
94def convert_graph(in_filename, out_filename, resolution=75., oversample=2.,
95 width=None, height=None, size=None):
97 _, tmp_filename_base = tempfile.mkstemp()
99 try:
100 if out_filename.endswith('.svg'):
101 fmt_arg = '-svg'
102 tmp_filename = tmp_filename_base
103 oversample = 1.0
104 else:
105 fmt_arg = '-png'
106 tmp_filename = tmp_filename_base + '-1.png'
108 if size is not None:
109 scale_args = ['-scale-to', '%i' % int(round(size*oversample))]
110 elif width is not None:
111 scale_args = ['-scale-to-x', '%i' % int(round(width*oversample))]
112 elif height is not None:
113 scale_args = ['-scale-to-y', '%i' % int(round(height*oversample))]
114 else:
115 scale_args = ['-r', '%i' % int(round(resolution * oversample))]
117 try:
118 subprocess.check_call(
119 ['pdftocairo'] + scale_args +
120 [fmt_arg, in_filename, tmp_filename_base])
121 except OSError as e:
122 raise GmtPyError(
123 'Cannot start `pdftocairo`, is it installed? (%s)' % str(e))
125 if oversample > 1.:
126 try:
127 subprocess.check_call([
128 'convert',
129 tmp_filename,
130 '-resize', '%i%%' % int(round(100.0/oversample)),
131 out_filename])
132 except OSError as e:
133 raise GmtPyError(
134 'Cannot start `convert`, is it installed? (%s)' % str(e))
136 else:
137 if out_filename.endswith('.png') or out_filename.endswith('.svg'):
138 shutil.move(tmp_filename, out_filename)
139 else:
140 try:
141 subprocess.check_call(
142 ['convert', tmp_filename, out_filename])
143 except Exception as e:
144 raise GmtPyError(
145 'Cannot start `convert`, is it installed? (%s)'
146 % str(e))
148 except Exception:
149 raise
151 finally:
152 if os.path.exists(tmp_filename_base):
153 os.remove(tmp_filename_base)
155 if os.path.exists(tmp_filename):
156 os.remove(tmp_filename)
159def get_bbox(s):
160 for pat in [find_hiresbb, find_bb]:
161 m = pat.search(s)
162 if m:
163 bb = [float(x) for x in m.group(1).split()]
164 return bb
166 raise GmtPyError('Cannot find bbox')
169def replace_bbox(bbox, *args):
171 def repl(m):
172 if m.group(1):
173 return ('%%HiResBoundingBox: ' + ' '.join(
174 '%.3f' % float(x) for x in bbox)).encode('ascii')
175 else:
176 return ('%%%%BoundingBox: %i %i %i %i' % (
177 int(math.floor(bbox[0])),
178 int(math.floor(bbox[1])),
179 int(math.ceil(bbox[2])),
180 int(math.ceil(bbox[3])))).encode('ascii')
182 pat = re.compile(br'%%(HiRes)?BoundingBox:((\s+[0-9.]+){4})')
183 if len(args) == 1:
184 s = args[0]
185 return pat.sub(repl, s)
187 else:
188 fin, fout = args
189 nn = 0
190 for line in fin:
191 line, n = pat.subn(repl, line)
192 nn += n
193 fout.write(line)
194 if nn == 2:
195 break
197 if nn == 2:
198 for line in fin:
199 fout.write(line)
202def escape_shell_arg(s):
203 '''
204 This function should be used for debugging output only - it could be
205 insecure.
206 '''
208 if re.search(r'[^a-zA-Z0-9._/=-]', s):
209 return "'" + s.replace("'", "'\\''") + "'"
210 else:
211 return s
214def escape_shell_args(args):
215 '''
216 This function should be used for debugging output only - it could be
217 insecure.
218 '''
220 return ' '.join([escape_shell_arg(x) for x in args])
223golden_ratio = 1.61803
225# units in points
226_units = {
227 'i': 72.,
228 'c': 72./2.54,
229 'm': 72.*100./2.54,
230 'p': 1.}
232inch = _units['i']
233cm = _units['c']
235# some awsome colors
236tango_colors = {
237 'butter1': (252, 233, 79),
238 'butter2': (237, 212, 0),
239 'butter3': (196, 160, 0),
240 'chameleon1': (138, 226, 52),
241 'chameleon2': (115, 210, 22),
242 'chameleon3': (78, 154, 6),
243 'orange1': (252, 175, 62),
244 'orange2': (245, 121, 0),
245 'orange3': (206, 92, 0),
246 'skyblue1': (114, 159, 207),
247 'skyblue2': (52, 101, 164),
248 'skyblue3': (32, 74, 135),
249 'plum1': (173, 127, 168),
250 'plum2': (117, 80, 123),
251 'plum3': (92, 53, 102),
252 'chocolate1': (233, 185, 110),
253 'chocolate2': (193, 125, 17),
254 'chocolate3': (143, 89, 2),
255 'scarletred1': (239, 41, 41),
256 'scarletred2': (204, 0, 0),
257 'scarletred3': (164, 0, 0),
258 'aluminium1': (238, 238, 236),
259 'aluminium2': (211, 215, 207),
260 'aluminium3': (186, 189, 182),
261 'aluminium4': (136, 138, 133),
262 'aluminium5': (85, 87, 83),
263 'aluminium6': (46, 52, 54)
264}
266graph_colors = [tango_colors[_x] for _x in (
267 'scarletred2', 'skyblue3', 'chameleon3', 'orange2', 'plum2', 'chocolate2',
268 'butter2')]
271def color(x=None):
272 '''
273 Generate a string for GMT option arguments expecting a color.
275 If ``x`` is None, a random color is returned. If it is an integer, the
276 corresponding ``gmtpy.graph_colors[x]`` or black returned. If it is a
277 string and the corresponding ``gmtpy.tango_colors[x]`` exists, this is
278 returned, or the string is passed through. If ``x`` is a tuple, it is
279 transformed into the string form which GMT expects.
280 '''
282 if x is None:
283 return '%i/%i/%i' % tuple(random.randint(0, 255) for _ in 'rgb')
285 if isinstance(x, int):
286 if 0 <= x < len(graph_colors):
287 return '%i/%i/%i' % graph_colors[x]
288 else:
289 return '0/0/0'
291 elif isinstance(x, str):
292 if x in tango_colors:
293 return '%i/%i/%i' % tango_colors[x]
294 else:
295 return x
297 return '%i/%i/%i' % x
300def color_tup(x=None):
301 if x is None:
302 return tuple([random.randint(0, 255) for _x in 'rgb'])
304 if isinstance(x, int):
305 if 0 <= x < len(graph_colors):
306 return graph_colors[x]
307 else:
308 return (0, 0, 0)
310 elif isinstance(x, str):
311 if x in tango_colors:
312 return tango_colors[x]
314 return x
317_gmt_installations = {}
319# Set fixed installation(s) to use...
320# (use this, if you want to use different GMT versions simultaneously.)
322# _gmt_installations['4.2.1'] = {'home': '/sw/etch-ia32/gmt-4.2.1',
323# 'bin': '/sw/etch-ia32/gmt-4.2.1/bin'}
324# _gmt_installations['4.3.0'] = {'home': '/sw/etch-ia32/gmt-4.3.0',
325# 'bin': '/sw/etch-ia32/gmt-4.3.0/bin'}
326# _gmt_installations['4.3.1'] = {'home': '/sw/share/gmt',
327# 'bin': '/sw/bin' }
329# ... or let GmtPy autodetect GMT via $PATH and $GMTHOME
332def key_version(a):
333 a = a.split('_')[0] # get rid of revision id
334 return [int(x) for x in a.split('.')]
337def newest_installed_gmt_version():
338 return sorted(_gmt_installations.keys(), key=key_version)[-1]
341def all_installed_gmt_versions():
342 return sorted(_gmt_installations.keys(), key=key_version)
345# To have consistent defaults, they are hardcoded here and should not be
346# changed.
348_gmt_defaults_by_version = {}
349_gmt_defaults_by_version['4.2.1'] = r'''
350#
351# GMT-SYSTEM 4.2.1 Defaults file
352#
353#-------- Plot Media Parameters -------------
354PAGE_COLOR = 255/255/255
355PAGE_ORIENTATION = portrait
356PAPER_MEDIA = a4+
357#-------- Basemap Annotation Parameters ------
358ANNOT_MIN_ANGLE = 20
359ANNOT_MIN_SPACING = 0
360ANNOT_FONT_PRIMARY = Helvetica
361ANNOT_FONT_SIZE = 12p
362ANNOT_OFFSET_PRIMARY = 0.075i
363ANNOT_FONT_SECONDARY = Helvetica
364ANNOT_FONT_SIZE_SECONDARY = 16p
365ANNOT_OFFSET_SECONDARY = 0.075i
366DEGREE_SYMBOL = ring
367HEADER_FONT = Helvetica
368HEADER_FONT_SIZE = 36p
369HEADER_OFFSET = 0.1875i
370LABEL_FONT = Helvetica
371LABEL_FONT_SIZE = 14p
372LABEL_OFFSET = 0.1125i
373OBLIQUE_ANNOTATION = 1
374PLOT_CLOCK_FORMAT = hh:mm:ss
375PLOT_DATE_FORMAT = yyyy-mm-dd
376PLOT_DEGREE_FORMAT = +ddd:mm:ss
377Y_AXIS_TYPE = hor_text
378#-------- Basemap Layout Parameters ---------
379BASEMAP_AXES = WESN
380BASEMAP_FRAME_RGB = 0/0/0
381BASEMAP_TYPE = plain
382FRAME_PEN = 1.25p
383FRAME_WIDTH = 0.075i
384GRID_CROSS_SIZE_PRIMARY = 0i
385GRID_CROSS_SIZE_SECONDARY = 0i
386GRID_PEN_PRIMARY = 0.25p
387GRID_PEN_SECONDARY = 0.5p
388MAP_SCALE_HEIGHT = 0.075i
389TICK_LENGTH = 0.075i
390POLAR_CAP = 85/90
391TICK_PEN = 0.5p
392X_AXIS_LENGTH = 9i
393Y_AXIS_LENGTH = 6i
394X_ORIGIN = 1i
395Y_ORIGIN = 1i
396UNIX_TIME = FALSE
397UNIX_TIME_POS = -0.75i/-0.75i
398#-------- Color System Parameters -----------
399COLOR_BACKGROUND = 0/0/0
400COLOR_FOREGROUND = 255/255/255
401COLOR_NAN = 128/128/128
402COLOR_IMAGE = adobe
403COLOR_MODEL = rgb
404HSV_MIN_SATURATION = 1
405HSV_MAX_SATURATION = 0.1
406HSV_MIN_VALUE = 0.3
407HSV_MAX_VALUE = 1
408#-------- PostScript Parameters -------------
409CHAR_ENCODING = ISOLatin1+
410DOTS_PR_INCH = 300
411N_COPIES = 1
412PS_COLOR = rgb
413PS_IMAGE_COMPRESS = none
414PS_IMAGE_FORMAT = ascii
415PS_LINE_CAP = round
416PS_LINE_JOIN = miter
417PS_MITER_LIMIT = 35
418PS_VERBOSE = FALSE
419GLOBAL_X_SCALE = 1
420GLOBAL_Y_SCALE = 1
421#-------- I/O Format Parameters -------------
422D_FORMAT = %lg
423FIELD_DELIMITER = tab
424GRIDFILE_SHORTHAND = FALSE
425GRID_FORMAT = nf
426INPUT_CLOCK_FORMAT = hh:mm:ss
427INPUT_DATE_FORMAT = yyyy-mm-dd
428IO_HEADER = FALSE
429N_HEADER_RECS = 1
430OUTPUT_CLOCK_FORMAT = hh:mm:ss
431OUTPUT_DATE_FORMAT = yyyy-mm-dd
432OUTPUT_DEGREE_FORMAT = +D
433XY_TOGGLE = FALSE
434#-------- Projection Parameters -------------
435ELLIPSOID = WGS-84
436MAP_SCALE_FACTOR = default
437MEASURE_UNIT = inch
438#-------- Calendar/Time Parameters ----------
439TIME_FORMAT_PRIMARY = full
440TIME_FORMAT_SECONDARY = full
441TIME_EPOCH = 2000-01-01T00:00:00
442TIME_IS_INTERVAL = OFF
443TIME_INTERVAL_FRACTION = 0.5
444TIME_LANGUAGE = us
445TIME_SYSTEM = other
446TIME_UNIT = d
447TIME_WEEK_START = Sunday
448Y2K_OFFSET_YEAR = 1950
449#-------- Miscellaneous Parameters ----------
450HISTORY = TRUE
451INTERPOLANT = akima
452LINE_STEP = 0.01i
453VECTOR_SHAPE = 0
454VERBOSE = FALSE'''
456_gmt_defaults_by_version['4.3.0'] = r'''
457#
458# GMT-SYSTEM 4.3.0 Defaults file
459#
460#-------- Plot Media Parameters -------------
461PAGE_COLOR = 255/255/255
462PAGE_ORIENTATION = portrait
463PAPER_MEDIA = a4+
464#-------- Basemap Annotation Parameters ------
465ANNOT_MIN_ANGLE = 20
466ANNOT_MIN_SPACING = 0
467ANNOT_FONT_PRIMARY = Helvetica
468ANNOT_FONT_SIZE_PRIMARY = 12p
469ANNOT_OFFSET_PRIMARY = 0.075i
470ANNOT_FONT_SECONDARY = Helvetica
471ANNOT_FONT_SIZE_SECONDARY = 16p
472ANNOT_OFFSET_SECONDARY = 0.075i
473DEGREE_SYMBOL = ring
474HEADER_FONT = Helvetica
475HEADER_FONT_SIZE = 36p
476HEADER_OFFSET = 0.1875i
477LABEL_FONT = Helvetica
478LABEL_FONT_SIZE = 14p
479LABEL_OFFSET = 0.1125i
480OBLIQUE_ANNOTATION = 1
481PLOT_CLOCK_FORMAT = hh:mm:ss
482PLOT_DATE_FORMAT = yyyy-mm-dd
483PLOT_DEGREE_FORMAT = +ddd:mm:ss
484Y_AXIS_TYPE = hor_text
485#-------- Basemap Layout Parameters ---------
486BASEMAP_AXES = WESN
487BASEMAP_FRAME_RGB = 0/0/0
488BASEMAP_TYPE = plain
489FRAME_PEN = 1.25p
490FRAME_WIDTH = 0.075i
491GRID_CROSS_SIZE_PRIMARY = 0i
492GRID_PEN_PRIMARY = 0.25p
493GRID_CROSS_SIZE_SECONDARY = 0i
494GRID_PEN_SECONDARY = 0.5p
495MAP_SCALE_HEIGHT = 0.075i
496POLAR_CAP = 85/90
497TICK_LENGTH = 0.075i
498TICK_PEN = 0.5p
499X_AXIS_LENGTH = 9i
500Y_AXIS_LENGTH = 6i
501X_ORIGIN = 1i
502Y_ORIGIN = 1i
503UNIX_TIME = FALSE
504UNIX_TIME_POS = BL/-0.75i/-0.75i
505UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
506#-------- Color System Parameters -----------
507COLOR_BACKGROUND = 0/0/0
508COLOR_FOREGROUND = 255/255/255
509COLOR_NAN = 128/128/128
510COLOR_IMAGE = adobe
511COLOR_MODEL = rgb
512HSV_MIN_SATURATION = 1
513HSV_MAX_SATURATION = 0.1
514HSV_MIN_VALUE = 0.3
515HSV_MAX_VALUE = 1
516#-------- PostScript Parameters -------------
517CHAR_ENCODING = ISOLatin1+
518DOTS_PR_INCH = 300
519N_COPIES = 1
520PS_COLOR = rgb
521PS_IMAGE_COMPRESS = none
522PS_IMAGE_FORMAT = ascii
523PS_LINE_CAP = round
524PS_LINE_JOIN = miter
525PS_MITER_LIMIT = 35
526PS_VERBOSE = FALSE
527GLOBAL_X_SCALE = 1
528GLOBAL_Y_SCALE = 1
529#-------- I/O Format Parameters -------------
530D_FORMAT = %lg
531FIELD_DELIMITER = tab
532GRIDFILE_SHORTHAND = FALSE
533GRID_FORMAT = nf
534INPUT_CLOCK_FORMAT = hh:mm:ss
535INPUT_DATE_FORMAT = yyyy-mm-dd
536IO_HEADER = FALSE
537N_HEADER_RECS = 1
538OUTPUT_CLOCK_FORMAT = hh:mm:ss
539OUTPUT_DATE_FORMAT = yyyy-mm-dd
540OUTPUT_DEGREE_FORMAT = +D
541XY_TOGGLE = FALSE
542#-------- Projection Parameters -------------
543ELLIPSOID = WGS-84
544MAP_SCALE_FACTOR = default
545MEASURE_UNIT = inch
546#-------- Calendar/Time Parameters ----------
547TIME_FORMAT_PRIMARY = full
548TIME_FORMAT_SECONDARY = full
549TIME_EPOCH = 2000-01-01T00:00:00
550TIME_IS_INTERVAL = OFF
551TIME_INTERVAL_FRACTION = 0.5
552TIME_LANGUAGE = us
553TIME_UNIT = d
554TIME_WEEK_START = Sunday
555Y2K_OFFSET_YEAR = 1950
556#-------- Miscellaneous Parameters ----------
557HISTORY = TRUE
558INTERPOLANT = akima
559LINE_STEP = 0.01i
560VECTOR_SHAPE = 0
561VERBOSE = FALSE'''
564_gmt_defaults_by_version['4.3.1'] = r'''
565#
566# GMT-SYSTEM 4.3.1 Defaults file
567#
568#-------- Plot Media Parameters -------------
569PAGE_COLOR = 255/255/255
570PAGE_ORIENTATION = portrait
571PAPER_MEDIA = a4+
572#-------- Basemap Annotation Parameters ------
573ANNOT_MIN_ANGLE = 20
574ANNOT_MIN_SPACING = 0
575ANNOT_FONT_PRIMARY = Helvetica
576ANNOT_FONT_SIZE_PRIMARY = 12p
577ANNOT_OFFSET_PRIMARY = 0.075i
578ANNOT_FONT_SECONDARY = Helvetica
579ANNOT_FONT_SIZE_SECONDARY = 16p
580ANNOT_OFFSET_SECONDARY = 0.075i
581DEGREE_SYMBOL = ring
582HEADER_FONT = Helvetica
583HEADER_FONT_SIZE = 36p
584HEADER_OFFSET = 0.1875i
585LABEL_FONT = Helvetica
586LABEL_FONT_SIZE = 14p
587LABEL_OFFSET = 0.1125i
588OBLIQUE_ANNOTATION = 1
589PLOT_CLOCK_FORMAT = hh:mm:ss
590PLOT_DATE_FORMAT = yyyy-mm-dd
591PLOT_DEGREE_FORMAT = +ddd:mm:ss
592Y_AXIS_TYPE = hor_text
593#-------- Basemap Layout Parameters ---------
594BASEMAP_AXES = WESN
595BASEMAP_FRAME_RGB = 0/0/0
596BASEMAP_TYPE = plain
597FRAME_PEN = 1.25p
598FRAME_WIDTH = 0.075i
599GRID_CROSS_SIZE_PRIMARY = 0i
600GRID_PEN_PRIMARY = 0.25p
601GRID_CROSS_SIZE_SECONDARY = 0i
602GRID_PEN_SECONDARY = 0.5p
603MAP_SCALE_HEIGHT = 0.075i
604POLAR_CAP = 85/90
605TICK_LENGTH = 0.075i
606TICK_PEN = 0.5p
607X_AXIS_LENGTH = 9i
608Y_AXIS_LENGTH = 6i
609X_ORIGIN = 1i
610Y_ORIGIN = 1i
611UNIX_TIME = FALSE
612UNIX_TIME_POS = BL/-0.75i/-0.75i
613UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
614#-------- Color System Parameters -----------
615COLOR_BACKGROUND = 0/0/0
616COLOR_FOREGROUND = 255/255/255
617COLOR_NAN = 128/128/128
618COLOR_IMAGE = adobe
619COLOR_MODEL = rgb
620HSV_MIN_SATURATION = 1
621HSV_MAX_SATURATION = 0.1
622HSV_MIN_VALUE = 0.3
623HSV_MAX_VALUE = 1
624#-------- PostScript Parameters -------------
625CHAR_ENCODING = ISOLatin1+
626DOTS_PR_INCH = 300
627N_COPIES = 1
628PS_COLOR = rgb
629PS_IMAGE_COMPRESS = none
630PS_IMAGE_FORMAT = ascii
631PS_LINE_CAP = round
632PS_LINE_JOIN = miter
633PS_MITER_LIMIT = 35
634PS_VERBOSE = FALSE
635GLOBAL_X_SCALE = 1
636GLOBAL_Y_SCALE = 1
637#-------- I/O Format Parameters -------------
638D_FORMAT = %lg
639FIELD_DELIMITER = tab
640GRIDFILE_SHORTHAND = FALSE
641GRID_FORMAT = nf
642INPUT_CLOCK_FORMAT = hh:mm:ss
643INPUT_DATE_FORMAT = yyyy-mm-dd
644IO_HEADER = FALSE
645N_HEADER_RECS = 1
646OUTPUT_CLOCK_FORMAT = hh:mm:ss
647OUTPUT_DATE_FORMAT = yyyy-mm-dd
648OUTPUT_DEGREE_FORMAT = +D
649XY_TOGGLE = FALSE
650#-------- Projection Parameters -------------
651ELLIPSOID = WGS-84
652MAP_SCALE_FACTOR = default
653MEASURE_UNIT = inch
654#-------- Calendar/Time Parameters ----------
655TIME_FORMAT_PRIMARY = full
656TIME_FORMAT_SECONDARY = full
657TIME_EPOCH = 2000-01-01T00:00:00
658TIME_IS_INTERVAL = OFF
659TIME_INTERVAL_FRACTION = 0.5
660TIME_LANGUAGE = us
661TIME_UNIT = d
662TIME_WEEK_START = Sunday
663Y2K_OFFSET_YEAR = 1950
664#-------- Miscellaneous Parameters ----------
665HISTORY = TRUE
666INTERPOLANT = akima
667LINE_STEP = 0.01i
668VECTOR_SHAPE = 0
669VERBOSE = FALSE'''
672_gmt_defaults_by_version['4.4.0'] = r'''
673#
674# GMT-SYSTEM 4.4.0 [64-bit] Defaults file
675#
676#-------- Plot Media Parameters -------------
677PAGE_COLOR = 255/255/255
678PAGE_ORIENTATION = portrait
679PAPER_MEDIA = a4+
680#-------- Basemap Annotation Parameters ------
681ANNOT_MIN_ANGLE = 20
682ANNOT_MIN_SPACING = 0
683ANNOT_FONT_PRIMARY = Helvetica
684ANNOT_FONT_SIZE_PRIMARY = 14p
685ANNOT_OFFSET_PRIMARY = 0.075i
686ANNOT_FONT_SECONDARY = Helvetica
687ANNOT_FONT_SIZE_SECONDARY = 16p
688ANNOT_OFFSET_SECONDARY = 0.075i
689DEGREE_SYMBOL = ring
690HEADER_FONT = Helvetica
691HEADER_FONT_SIZE = 36p
692HEADER_OFFSET = 0.1875i
693LABEL_FONT = Helvetica
694LABEL_FONT_SIZE = 14p
695LABEL_OFFSET = 0.1125i
696OBLIQUE_ANNOTATION = 1
697PLOT_CLOCK_FORMAT = hh:mm:ss
698PLOT_DATE_FORMAT = yyyy-mm-dd
699PLOT_DEGREE_FORMAT = +ddd:mm:ss
700Y_AXIS_TYPE = hor_text
701#-------- Basemap Layout Parameters ---------
702BASEMAP_AXES = WESN
703BASEMAP_FRAME_RGB = 0/0/0
704BASEMAP_TYPE = plain
705FRAME_PEN = 1.25p
706FRAME_WIDTH = 0.075i
707GRID_CROSS_SIZE_PRIMARY = 0i
708GRID_PEN_PRIMARY = 0.25p
709GRID_CROSS_SIZE_SECONDARY = 0i
710GRID_PEN_SECONDARY = 0.5p
711MAP_SCALE_HEIGHT = 0.075i
712POLAR_CAP = 85/90
713TICK_LENGTH = 0.075i
714TICK_PEN = 0.5p
715X_AXIS_LENGTH = 9i
716Y_AXIS_LENGTH = 6i
717X_ORIGIN = 1i
718Y_ORIGIN = 1i
719UNIX_TIME = FALSE
720UNIX_TIME_POS = BL/-0.75i/-0.75i
721UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
722#-------- Color System Parameters -----------
723COLOR_BACKGROUND = 0/0/0
724COLOR_FOREGROUND = 255/255/255
725COLOR_NAN = 128/128/128
726COLOR_IMAGE = adobe
727COLOR_MODEL = rgb
728HSV_MIN_SATURATION = 1
729HSV_MAX_SATURATION = 0.1
730HSV_MIN_VALUE = 0.3
731HSV_MAX_VALUE = 1
732#-------- PostScript Parameters -------------
733CHAR_ENCODING = ISOLatin1+
734DOTS_PR_INCH = 300
735N_COPIES = 1
736PS_COLOR = rgb
737PS_IMAGE_COMPRESS = lzw
738PS_IMAGE_FORMAT = ascii
739PS_LINE_CAP = round
740PS_LINE_JOIN = miter
741PS_MITER_LIMIT = 35
742PS_VERBOSE = FALSE
743GLOBAL_X_SCALE = 1
744GLOBAL_Y_SCALE = 1
745#-------- I/O Format Parameters -------------
746D_FORMAT = %lg
747FIELD_DELIMITER = tab
748GRIDFILE_SHORTHAND = FALSE
749GRID_FORMAT = nf
750INPUT_CLOCK_FORMAT = hh:mm:ss
751INPUT_DATE_FORMAT = yyyy-mm-dd
752IO_HEADER = FALSE
753N_HEADER_RECS = 1
754OUTPUT_CLOCK_FORMAT = hh:mm:ss
755OUTPUT_DATE_FORMAT = yyyy-mm-dd
756OUTPUT_DEGREE_FORMAT = +D
757XY_TOGGLE = FALSE
758#-------- Projection Parameters -------------
759ELLIPSOID = WGS-84
760MAP_SCALE_FACTOR = default
761MEASURE_UNIT = inch
762#-------- Calendar/Time Parameters ----------
763TIME_FORMAT_PRIMARY = full
764TIME_FORMAT_SECONDARY = full
765TIME_EPOCH = 2000-01-01T00:00:00
766TIME_IS_INTERVAL = OFF
767TIME_INTERVAL_FRACTION = 0.5
768TIME_LANGUAGE = us
769TIME_UNIT = d
770TIME_WEEK_START = Sunday
771Y2K_OFFSET_YEAR = 1950
772#-------- Miscellaneous Parameters ----------
773HISTORY = TRUE
774INTERPOLANT = akima
775LINE_STEP = 0.01i
776VECTOR_SHAPE = 0
777VERBOSE = FALSE
778'''
780_gmt_defaults_by_version['4.5.2'] = r'''
781#
782# GMT-SYSTEM 4.5.2 [64-bit] Defaults file
783#
784#-------- Plot Media Parameters -------------
785PAGE_COLOR = white
786PAGE_ORIENTATION = portrait
787PAPER_MEDIA = a4+
788#-------- Basemap Annotation Parameters ------
789ANNOT_MIN_ANGLE = 20
790ANNOT_MIN_SPACING = 0
791ANNOT_FONT_PRIMARY = Helvetica
792ANNOT_FONT_SIZE_PRIMARY = 14p
793ANNOT_OFFSET_PRIMARY = 0.075i
794ANNOT_FONT_SECONDARY = Helvetica
795ANNOT_FONT_SIZE_SECONDARY = 16p
796ANNOT_OFFSET_SECONDARY = 0.075i
797DEGREE_SYMBOL = ring
798HEADER_FONT = Helvetica
799HEADER_FONT_SIZE = 36p
800HEADER_OFFSET = 0.1875i
801LABEL_FONT = Helvetica
802LABEL_FONT_SIZE = 14p
803LABEL_OFFSET = 0.1125i
804OBLIQUE_ANNOTATION = 1
805PLOT_CLOCK_FORMAT = hh:mm:ss
806PLOT_DATE_FORMAT = yyyy-mm-dd
807PLOT_DEGREE_FORMAT = +ddd:mm:ss
808Y_AXIS_TYPE = hor_text
809#-------- Basemap Layout Parameters ---------
810BASEMAP_AXES = WESN
811BASEMAP_FRAME_RGB = black
812BASEMAP_TYPE = plain
813FRAME_PEN = 1.25p
814FRAME_WIDTH = 0.075i
815GRID_CROSS_SIZE_PRIMARY = 0i
816GRID_PEN_PRIMARY = 0.25p
817GRID_CROSS_SIZE_SECONDARY = 0i
818GRID_PEN_SECONDARY = 0.5p
819MAP_SCALE_HEIGHT = 0.075i
820POLAR_CAP = 85/90
821TICK_LENGTH = 0.075i
822TICK_PEN = 0.5p
823X_AXIS_LENGTH = 9i
824Y_AXIS_LENGTH = 6i
825X_ORIGIN = 1i
826Y_ORIGIN = 1i
827UNIX_TIME = FALSE
828UNIX_TIME_POS = BL/-0.75i/-0.75i
829UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
830#-------- Color System Parameters -----------
831COLOR_BACKGROUND = black
832COLOR_FOREGROUND = white
833COLOR_NAN = 128
834COLOR_IMAGE = adobe
835COLOR_MODEL = rgb
836HSV_MIN_SATURATION = 1
837HSV_MAX_SATURATION = 0.1
838HSV_MIN_VALUE = 0.3
839HSV_MAX_VALUE = 1
840#-------- PostScript Parameters -------------
841CHAR_ENCODING = ISOLatin1+
842DOTS_PR_INCH = 300
843GLOBAL_X_SCALE = 1
844GLOBAL_Y_SCALE = 1
845N_COPIES = 1
846PS_COLOR = rgb
847PS_IMAGE_COMPRESS = lzw
848PS_IMAGE_FORMAT = ascii
849PS_LINE_CAP = round
850PS_LINE_JOIN = miter
851PS_MITER_LIMIT = 35
852PS_VERBOSE = FALSE
853TRANSPARENCY = 0
854#-------- I/O Format Parameters -------------
855D_FORMAT = %.12lg
856FIELD_DELIMITER = tab
857GRIDFILE_FORMAT = nf
858GRIDFILE_SHORTHAND = FALSE
859INPUT_CLOCK_FORMAT = hh:mm:ss
860INPUT_DATE_FORMAT = yyyy-mm-dd
861IO_HEADER = FALSE
862N_HEADER_RECS = 1
863NAN_RECORDS = pass
864OUTPUT_CLOCK_FORMAT = hh:mm:ss
865OUTPUT_DATE_FORMAT = yyyy-mm-dd
866OUTPUT_DEGREE_FORMAT = D
867XY_TOGGLE = FALSE
868#-------- Projection Parameters -------------
869ELLIPSOID = WGS-84
870MAP_SCALE_FACTOR = default
871MEASURE_UNIT = inch
872#-------- Calendar/Time Parameters ----------
873TIME_FORMAT_PRIMARY = full
874TIME_FORMAT_SECONDARY = full
875TIME_EPOCH = 2000-01-01T00:00:00
876TIME_IS_INTERVAL = OFF
877TIME_INTERVAL_FRACTION = 0.5
878TIME_LANGUAGE = us
879TIME_UNIT = d
880TIME_WEEK_START = Sunday
881Y2K_OFFSET_YEAR = 1950
882#-------- Miscellaneous Parameters ----------
883HISTORY = TRUE
884INTERPOLANT = akima
885LINE_STEP = 0.01i
886VECTOR_SHAPE = 0
887VERBOSE = FALSE
888'''
890_gmt_defaults_by_version['4.5.3'] = r'''
891#
892# GMT-SYSTEM 4.5.3 (CVS Jun 18 2010 10:56:07) [64-bit] Defaults file
893#
894#-------- Plot Media Parameters -------------
895PAGE_COLOR = white
896PAGE_ORIENTATION = portrait
897PAPER_MEDIA = a4+
898#-------- Basemap Annotation Parameters ------
899ANNOT_MIN_ANGLE = 20
900ANNOT_MIN_SPACING = 0
901ANNOT_FONT_PRIMARY = Helvetica
902ANNOT_FONT_SIZE_PRIMARY = 14p
903ANNOT_OFFSET_PRIMARY = 0.075i
904ANNOT_FONT_SECONDARY = Helvetica
905ANNOT_FONT_SIZE_SECONDARY = 16p
906ANNOT_OFFSET_SECONDARY = 0.075i
907DEGREE_SYMBOL = ring
908HEADER_FONT = Helvetica
909HEADER_FONT_SIZE = 36p
910HEADER_OFFSET = 0.1875i
911LABEL_FONT = Helvetica
912LABEL_FONT_SIZE = 14p
913LABEL_OFFSET = 0.1125i
914OBLIQUE_ANNOTATION = 1
915PLOT_CLOCK_FORMAT = hh:mm:ss
916PLOT_DATE_FORMAT = yyyy-mm-dd
917PLOT_DEGREE_FORMAT = +ddd:mm:ss
918Y_AXIS_TYPE = hor_text
919#-------- Basemap Layout Parameters ---------
920BASEMAP_AXES = WESN
921BASEMAP_FRAME_RGB = black
922BASEMAP_TYPE = plain
923FRAME_PEN = 1.25p
924FRAME_WIDTH = 0.075i
925GRID_CROSS_SIZE_PRIMARY = 0i
926GRID_PEN_PRIMARY = 0.25p
927GRID_CROSS_SIZE_SECONDARY = 0i
928GRID_PEN_SECONDARY = 0.5p
929MAP_SCALE_HEIGHT = 0.075i
930POLAR_CAP = 85/90
931TICK_LENGTH = 0.075i
932TICK_PEN = 0.5p
933X_AXIS_LENGTH = 9i
934Y_AXIS_LENGTH = 6i
935X_ORIGIN = 1i
936Y_ORIGIN = 1i
937UNIX_TIME = FALSE
938UNIX_TIME_POS = BL/-0.75i/-0.75i
939UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
940#-------- Color System Parameters -----------
941COLOR_BACKGROUND = black
942COLOR_FOREGROUND = white
943COLOR_NAN = 128
944COLOR_IMAGE = adobe
945COLOR_MODEL = rgb
946HSV_MIN_SATURATION = 1
947HSV_MAX_SATURATION = 0.1
948HSV_MIN_VALUE = 0.3
949HSV_MAX_VALUE = 1
950#-------- PostScript Parameters -------------
951CHAR_ENCODING = ISOLatin1+
952DOTS_PR_INCH = 300
953GLOBAL_X_SCALE = 1
954GLOBAL_Y_SCALE = 1
955N_COPIES = 1
956PS_COLOR = rgb
957PS_IMAGE_COMPRESS = lzw
958PS_IMAGE_FORMAT = ascii
959PS_LINE_CAP = round
960PS_LINE_JOIN = miter
961PS_MITER_LIMIT = 35
962PS_VERBOSE = FALSE
963TRANSPARENCY = 0
964#-------- I/O Format Parameters -------------
965D_FORMAT = %.12lg
966FIELD_DELIMITER = tab
967GRIDFILE_FORMAT = nf
968GRIDFILE_SHORTHAND = FALSE
969INPUT_CLOCK_FORMAT = hh:mm:ss
970INPUT_DATE_FORMAT = yyyy-mm-dd
971IO_HEADER = FALSE
972N_HEADER_RECS = 1
973NAN_RECORDS = pass
974OUTPUT_CLOCK_FORMAT = hh:mm:ss
975OUTPUT_DATE_FORMAT = yyyy-mm-dd
976OUTPUT_DEGREE_FORMAT = D
977XY_TOGGLE = FALSE
978#-------- Projection Parameters -------------
979ELLIPSOID = WGS-84
980MAP_SCALE_FACTOR = default
981MEASURE_UNIT = inch
982#-------- Calendar/Time Parameters ----------
983TIME_FORMAT_PRIMARY = full
984TIME_FORMAT_SECONDARY = full
985TIME_EPOCH = 2000-01-01T00:00:00
986TIME_IS_INTERVAL = OFF
987TIME_INTERVAL_FRACTION = 0.5
988TIME_LANGUAGE = us
989TIME_UNIT = d
990TIME_WEEK_START = Sunday
991Y2K_OFFSET_YEAR = 1950
992#-------- Miscellaneous Parameters ----------
993HISTORY = TRUE
994INTERPOLANT = akima
995LINE_STEP = 0.01i
996VECTOR_SHAPE = 0
997VERBOSE = FALSE
998'''
1000_gmt_defaults_by_version['5.1.2'] = r'''
1001#
1002# GMT 5.1.2 Defaults file
1003# vim:sw=8:ts=8:sts=8
1004# $Revision: 13836 $
1005# $LastChangedDate: 2014-12-20 03:45:42 -1000 (Sat, 20 Dec 2014) $
1006#
1007# COLOR Parameters
1008#
1009COLOR_BACKGROUND = black
1010COLOR_FOREGROUND = white
1011COLOR_NAN = 127.5
1012COLOR_MODEL = none
1013COLOR_HSV_MIN_S = 1
1014COLOR_HSV_MAX_S = 0.1
1015COLOR_HSV_MIN_V = 0.3
1016COLOR_HSV_MAX_V = 1
1017#
1018# DIR Parameters
1019#
1020DIR_DATA =
1021DIR_DCW =
1022DIR_GSHHG =
1023#
1024# FONT Parameters
1025#
1026FONT_ANNOT_PRIMARY = 14p,Helvetica,black
1027FONT_ANNOT_SECONDARY = 16p,Helvetica,black
1028FONT_LABEL = 14p,Helvetica,black
1029FONT_LOGO = 8p,Helvetica,black
1030FONT_TITLE = 24p,Helvetica,black
1031#
1032# FORMAT Parameters
1033#
1034FORMAT_CLOCK_IN = hh:mm:ss
1035FORMAT_CLOCK_OUT = hh:mm:ss
1036FORMAT_CLOCK_MAP = hh:mm:ss
1037FORMAT_DATE_IN = yyyy-mm-dd
1038FORMAT_DATE_OUT = yyyy-mm-dd
1039FORMAT_DATE_MAP = yyyy-mm-dd
1040FORMAT_GEO_OUT = D
1041FORMAT_GEO_MAP = ddd:mm:ss
1042FORMAT_FLOAT_OUT = %.12g
1043FORMAT_FLOAT_MAP = %.12g
1044FORMAT_TIME_PRIMARY_MAP = full
1045FORMAT_TIME_SECONDARY_MAP = full
1046FORMAT_TIME_STAMP = %Y %b %d %H:%M:%S
1047#
1048# GMT Miscellaneous Parameters
1049#
1050GMT_COMPATIBILITY = 4
1051GMT_CUSTOM_LIBS =
1052GMT_EXTRAPOLATE_VAL = NaN
1053GMT_FFT = auto
1054GMT_HISTORY = true
1055GMT_INTERPOLANT = akima
1056GMT_TRIANGULATE = Shewchuk
1057GMT_VERBOSE = compat
1058GMT_LANGUAGE = us
1059#
1060# I/O Parameters
1061#
1062IO_COL_SEPARATOR = tab
1063IO_GRIDFILE_FORMAT = nf
1064IO_GRIDFILE_SHORTHAND = false
1065IO_HEADER = false
1066IO_N_HEADER_RECS = 0
1067IO_NAN_RECORDS = pass
1068IO_NC4_CHUNK_SIZE = auto
1069IO_NC4_DEFLATION_LEVEL = 3
1070IO_LONLAT_TOGGLE = false
1071IO_SEGMENT_MARKER = >
1072#
1073# MAP Parameters
1074#
1075MAP_ANNOT_MIN_ANGLE = 20
1076MAP_ANNOT_MIN_SPACING = 0p
1077MAP_ANNOT_OBLIQUE = 1
1078MAP_ANNOT_OFFSET_PRIMARY = 0.075i
1079MAP_ANNOT_OFFSET_SECONDARY = 0.075i
1080MAP_ANNOT_ORTHO = we
1081MAP_DEFAULT_PEN = default,black
1082MAP_DEGREE_SYMBOL = ring
1083MAP_FRAME_AXES = WESNZ
1084MAP_FRAME_PEN = thicker,black
1085MAP_FRAME_TYPE = fancy
1086MAP_FRAME_WIDTH = 5p
1087MAP_GRID_CROSS_SIZE_PRIMARY = 0p
1088MAP_GRID_CROSS_SIZE_SECONDARY = 0p
1089MAP_GRID_PEN_PRIMARY = default,black
1090MAP_GRID_PEN_SECONDARY = thinner,black
1091MAP_LABEL_OFFSET = 0.1944i
1092MAP_LINE_STEP = 0.75p
1093MAP_LOGO = false
1094MAP_LOGO_POS = BL/-54p/-54p
1095MAP_ORIGIN_X = 1i
1096MAP_ORIGIN_Y = 1i
1097MAP_POLAR_CAP = 85/90
1098MAP_SCALE_HEIGHT = 5p
1099MAP_TICK_LENGTH_PRIMARY = 5p/2.5p
1100MAP_TICK_LENGTH_SECONDARY = 15p/3.75p
1101MAP_TICK_PEN_PRIMARY = thinner,black
1102MAP_TICK_PEN_SECONDARY = thinner,black
1103MAP_TITLE_OFFSET = 14p
1104MAP_VECTOR_SHAPE = 0
1105#
1106# Projection Parameters
1107#
1108PROJ_AUX_LATITUDE = authalic
1109PROJ_ELLIPSOID = WGS-84
1110PROJ_LENGTH_UNIT = cm
1111PROJ_MEAN_RADIUS = authalic
1112PROJ_SCALE_FACTOR = default
1113#
1114# PostScript Parameters
1115#
1116PS_CHAR_ENCODING = ISOLatin1+
1117PS_COLOR_MODEL = rgb
1118PS_COMMENTS = false
1119PS_IMAGE_COMPRESS = deflate,5
1120PS_LINE_CAP = butt
1121PS_LINE_JOIN = miter
1122PS_MITER_LIMIT = 35
1123PS_MEDIA = a4
1124PS_PAGE_COLOR = white
1125PS_PAGE_ORIENTATION = portrait
1126PS_SCALE_X = 1
1127PS_SCALE_Y = 1
1128PS_TRANSPARENCY = Normal
1129#
1130# Calendar/Time Parameters
1131#
1132TIME_EPOCH = 1970-01-01T00:00:00
1133TIME_IS_INTERVAL = off
1134TIME_INTERVAL_FRACTION = 0.5
1135TIME_UNIT = s
1136TIME_WEEK_START = Monday
1137TIME_Y2K_OFFSET_YEAR = 1950
1138'''
1141def get_gmt_version(gmtdefaultsbinary, gmthomedir=None):
1142 args = [gmtdefaultsbinary]
1144 environ = os.environ.copy()
1145 environ['GMTHOME'] = gmthomedir or ''
1147 p = subprocess.Popen(
1148 args,
1149 stdout=subprocess.PIPE,
1150 stderr=subprocess.PIPE,
1151 env=environ)
1153 (stdout, stderr) = p.communicate()
1154 m = re.search(br'(\d+(\.\d+)*)', stderr) \
1155 or re.search(br'# GMT (\d+(\.\d+)*)', stdout)
1157 if not m:
1158 raise GMTInstallationProblem(
1159 "Can't extract version number from output of %s."
1160 % gmtdefaultsbinary)
1162 return str(m.group(1).decode('ascii'))
1165def detect_gmt_installations():
1167 installations = {}
1168 errmesses = []
1170 # GMT 4.x:
1171 try:
1172 p = subprocess.Popen(
1173 ['GMT'],
1174 stdout=subprocess.PIPE,
1175 stderr=subprocess.PIPE)
1177 (stdout, stderr) = p.communicate()
1179 m = re.search(br'Version\s+(\d+(\.\d+)*)', stderr, re.M)
1180 if not m:
1181 raise GMTInstallationProblem(
1182 "Can't get version number from output of GMT.")
1184 version = str(m.group(1).decode('ascii'))
1185 if version[0] != '5':
1187 m = re.search(br'^\s+executables\s+(.+)$', stderr, re.M)
1188 if not m:
1189 raise GMTInstallationProblem(
1190 "Can't extract executables dir from output of GMT.")
1192 gmtbin = str(m.group(1).decode('ascii'))
1194 m = re.search(br'^\s+shared data\s+(.+)$', stderr, re.M)
1195 if not m:
1196 raise GMTInstallationProblem(
1197 "Can't extract shared dir from output of GMT.")
1199 gmtshare = str(m.group(1).decode('ascii'))
1200 if not gmtshare.endswith('/share'):
1201 raise GMTInstallationProblem(
1202 "Can't determine GMTHOME from output of GMT.")
1204 gmthome = gmtshare[:-6]
1206 installations[version] = {
1207 'home': gmthome,
1208 'bin': gmtbin}
1210 except OSError as e:
1211 errmesses.append(('GMT', str(e)))
1213 try:
1214 version = str(subprocess.check_output(
1215 ['gmt', '--version']).strip().decode('ascii')).split('_')[0]
1216 gmtbin = str(subprocess.check_output(
1217 ['gmt', '--show-bindir']).strip().decode('ascii'))
1218 installations[version] = {
1219 'bin': gmtbin}
1221 except (OSError, subprocess.CalledProcessError) as e:
1222 errmesses.append(('gmt', str(e)))
1224 if not installations:
1225 s = []
1226 for (progname, errmess) in errmesses:
1227 s.append('Cannot start "%s" executable: %s' % (progname, errmess))
1229 raise GMTInstallationProblem(', '.join(s))
1231 return installations
1234def appropriate_defaults_version(version):
1235 avails = sorted(_gmt_defaults_by_version.keys(), key=key_version)
1236 for iavail, avail in enumerate(avails):
1237 if key_version(version) == key_version(avail):
1238 return version
1240 elif key_version(version) < key_version(avail):
1241 return avails[max(0, iavail-1)]
1243 return avails[-1]
1246def gmt_default_config(version):
1247 '''
1248 Get default GMT configuration dict for given version.
1249 '''
1251 xversion = appropriate_defaults_version(version)
1253 # if not version in _gmt_defaults_by_version:
1254 # raise GMTError('No GMT defaults for version %s found' % version)
1256 gmt_defaults = _gmt_defaults_by_version[xversion]
1258 d = {}
1259 for line in gmt_defaults.splitlines():
1260 sline = line.strip()
1261 if not sline or sline.startswith('#'):
1262 continue
1264 k, v = sline.split('=', 1)
1265 d[k.strip()] = v.strip()
1267 return d
1270def diff_defaults(v1, v2):
1271 d1 = gmt_default_config(v1)
1272 d2 = gmt_default_config(v2)
1273 for k in d1:
1274 if k not in d2:
1275 print('%s not in %s' % (k, v2))
1276 else:
1277 if d1[k] != d2[k]:
1278 print('%s %s = %s' % (v1, k, d1[k]))
1279 print('%s %s = %s' % (v2, k, d2[k]))
1281 for k in d2:
1282 if k not in d1:
1283 print('%s not in %s' % (k, v1))
1285# diff_defaults('4.5.2', '4.5.3')
1288def check_gmt_installation(installation):
1290 home_dir = installation.get('home', None)
1291 bin_dir = installation['bin']
1292 version = installation['version']
1294 for d in home_dir, bin_dir:
1295 if d is not None:
1296 if not os.path.exists(d):
1297 logging.error(('Directory does not exist: %s\n'
1298 'Check your GMT installation.') % d)
1300 major_version = version.split('.')[0]
1301 if major_version == '6':
1302 raise GMTInstallationProblem(
1303 'GMT version 6 not supported by pyrocko.gmtpy.')
1305 if major_version != '5':
1306 gmtdefaults = pjoin(bin_dir, 'gmtdefaults')
1308 versionfound = get_gmt_version(gmtdefaults, home_dir)
1310 if versionfound != version:
1311 raise GMTInstallationProblem((
1312 'Expected GMT version %s but found version %s.\n'
1313 '(Looking at output of %s)') % (
1314 version, versionfound, gmtdefaults))
1317def get_gmt_installation(version):
1318 setup_gmt_installations()
1319 if version != 'newest' and version not in _gmt_installations:
1320 logging.warn('GMT version %s not installed, taking version %s instead'
1321 % (version, newest_installed_gmt_version()))
1323 version = 'newest'
1325 if version == 'newest':
1326 version = newest_installed_gmt_version()
1328 installation = dict(_gmt_installations[version])
1330 return installation
1333def setup_gmt_installations():
1334 if not setup_gmt_installations.have_done:
1335 if not _gmt_installations:
1337 _gmt_installations.update(detect_gmt_installations())
1339 # store defaults as dicts into the gmt installations dicts
1340 for version, installation in _gmt_installations.items():
1341 installation['defaults'] = gmt_default_config(version)
1342 installation['version'] = version
1344 for installation in _gmt_installations.values():
1345 check_gmt_installation(installation)
1347 setup_gmt_installations.have_done = True
1350setup_gmt_installations.have_done = False
1352_paper_sizes_a = '''A0 2380 3368
1353 A1 1684 2380
1354 A2 1190 1684
1355 A3 842 1190
1356 A4 595 842
1357 A5 421 595
1358 A6 297 421
1359 A7 210 297
1360 A8 148 210
1361 A9 105 148
1362 A10 74 105
1363 B0 2836 4008
1364 B1 2004 2836
1365 B2 1418 2004
1366 B3 1002 1418
1367 B4 709 1002
1368 B5 501 709
1369 archA 648 864
1370 archB 864 1296
1371 archC 1296 1728
1372 archD 1728 2592
1373 archE 2592 3456
1374 flsa 612 936
1375 halfletter 396 612
1376 note 540 720
1377 letter 612 792
1378 legal 612 1008
1379 11x17 792 1224
1380 ledger 1224 792'''
1383_paper_sizes = {}
1386def setup_paper_sizes():
1387 if not _paper_sizes:
1388 for line in _paper_sizes_a.splitlines():
1389 k, w, h = line.split()
1390 _paper_sizes[k.lower()] = float(w), float(h)
1393def get_paper_size(k):
1394 setup_paper_sizes()
1395 return _paper_sizes[k.lower().rstrip('+')]
1398def all_paper_sizes():
1399 setup_paper_sizes()
1400 return _paper_sizes
1403def measure_unit(gmt_config):
1404 for k in ['MEASURE_UNIT', 'PROJ_LENGTH_UNIT']:
1405 if k in gmt_config:
1406 return gmt_config[k]
1408 raise GmtPyError('cannot get measure unit / proj length unit from config')
1411def paper_media(gmt_config):
1412 for k in ['PAPER_MEDIA', 'PS_MEDIA']:
1413 if k in gmt_config:
1414 return gmt_config[k]
1416 raise GmtPyError('cannot get paper media from config')
1419def page_orientation(gmt_config):
1420 for k in ['PAGE_ORIENTATION', 'PS_PAGE_ORIENTATION']:
1421 if k in gmt_config:
1422 return gmt_config[k]
1424 raise GmtPyError('cannot get paper orientation from config')
1427def make_bbox(width, height, gmt_config, margins=(0.8, 0.8, 0.8, 0.8)):
1429 leftmargin, topmargin, rightmargin, bottommargin = margins
1430 portrait = page_orientation(gmt_config).lower() == 'portrait'
1432 paper_size = get_paper_size(paper_media(gmt_config))
1433 if not portrait:
1434 paper_size = paper_size[1], paper_size[0]
1436 xoffset = (paper_size[0] - (width + leftmargin + rightmargin)) / \
1437 2.0 + leftmargin
1438 yoffset = (paper_size[1] - (height + topmargin + bottommargin)) / \
1439 2.0 + bottommargin
1441 if portrait:
1442 bb1 = int((xoffset - leftmargin))
1443 bb2 = int((yoffset - bottommargin))
1444 bb3 = bb1 + int((width+leftmargin+rightmargin))
1445 bb4 = bb2 + int((height+topmargin+bottommargin))
1446 else:
1447 bb1 = int((yoffset - topmargin))
1448 bb2 = int((xoffset - leftmargin))
1449 bb3 = bb1 + int((height+topmargin+bottommargin))
1450 bb4 = bb2 + int((width+leftmargin+rightmargin))
1452 return xoffset, yoffset, (bb1, bb2, bb3, bb4)
1455def gmtdefaults_as_text(version='newest'):
1457 '''
1458 Get the built-in gmtdefaults.
1459 '''
1461 if version not in _gmt_installations:
1462 logging.warn('GMT version %s not installed, taking version %s instead'
1463 % (version, newest_installed_gmt_version()))
1464 version = 'newest'
1466 if version == 'newest':
1467 version = newest_installed_gmt_version()
1469 return _gmt_defaults_by_version[version]
1472def savegrd(x, y, z, filename, title=None, naming='xy'):
1473 '''
1474 Write COARDS compliant netcdf (grd) file.
1475 '''
1477 assert y.size, x.size == z.shape
1478 ny, nx = z.shape
1479 nc = netcdf.netcdf_file(filename, 'w')
1480 assert naming in ('xy', 'lonlat')
1482 if naming == 'xy':
1483 kx, ky = 'x', 'y'
1484 else:
1485 kx, ky = 'lon', 'lat'
1487 nc.node_offset = 0
1488 if title is not None:
1489 nc.title = title
1491 nc.Conventions = 'COARDS/CF-1.0'
1492 nc.createDimension(kx, nx)
1493 nc.createDimension(ky, ny)
1495 xvar = nc.createVariable(kx, 'd', (kx,))
1496 yvar = nc.createVariable(ky, 'd', (ky,))
1497 if naming == 'xy':
1498 xvar.long_name = kx
1499 yvar.long_name = ky
1500 else:
1501 xvar.long_name = 'longitude'
1502 xvar.units = 'degrees_east'
1503 yvar.long_name = 'latitude'
1504 yvar.units = 'degrees_north'
1506 zvar = nc.createVariable('z', 'd', (ky, kx))
1508 xvar[:] = x.astype(num.float64)
1509 yvar[:] = y.astype(num.float64)
1510 zvar[:] = z.astype(num.float64)
1512 nc.close()
1515def to_array(var):
1516 arr = var[:].copy()
1517 if hasattr(var, 'scale_factor'):
1518 arr *= var.scale_factor
1520 if hasattr(var, 'add_offset'):
1521 arr += var.add_offset
1523 return arr
1526def loadgrd(filename):
1527 '''
1528 Read COARDS compliant netcdf (grd) file.
1529 '''
1531 nc = netcdf.netcdf_file(filename, 'r')
1532 vkeys = list(nc.variables.keys())
1533 kx = 'x'
1534 ky = 'y'
1535 if 'lon' in vkeys:
1536 kx = 'lon'
1537 if 'lat' in vkeys:
1538 ky = 'lat'
1540 kz = 'z'
1541 if 'altitude' in vkeys:
1542 kz = 'altitude'
1544 x = to_array(nc.variables[kx])
1545 y = to_array(nc.variables[ky])
1546 z = to_array(nc.variables[kz])
1548 nc.close()
1549 return x, y, z
1552def centers_to_edges(asorted):
1553 return (asorted[1:] + asorted[:-1])/2.
1556def nvals(asorted):
1557 eps = (asorted[-1]-asorted[0])/asorted.size
1558 return num.sum(asorted[1:] - asorted[:-1] >= eps) + 1
1561def guess_vals(asorted):
1562 eps = (asorted[-1]-asorted[0])/asorted.size
1563 indis = num.nonzero(asorted[1:] - asorted[:-1] >= eps)[0]
1564 indis = num.concatenate((num.array([0]), indis+1,
1565 num.array([asorted.size])))
1566 asum = num.zeros(asorted.size+1)
1567 asum[1:] = num.cumsum(asorted)
1568 return (asum[indis[1:]] - asum[indis[:-1]]) / (indis[1:]-indis[:-1])
1571def blockmean(asorted, b):
1572 indis = num.nonzero(asorted[1:] - asorted[:-1])[0]
1573 indis = num.concatenate((num.array([0]), indis+1,
1574 num.array([asorted.size])))
1575 bsum = num.zeros(b.size+1)
1576 bsum[1:] = num.cumsum(b)
1577 return (
1578 asorted[indis[:-1]],
1579 (bsum[indis[1:]] - bsum[indis[:-1]]) / (indis[1:]-indis[:-1]))
1582def griddata_regular(x, y, z, xvals, yvals):
1583 nx, ny = xvals.size, yvals.size
1584 xindi = num.digitize(x, centers_to_edges(xvals))
1585 yindi = num.digitize(y, centers_to_edges(yvals))
1587 zindi = yindi*nx+xindi
1588 order = num.argsort(zindi)
1589 z = z[order]
1590 zindi = zindi[order]
1592 zindi, z = blockmean(zindi, z)
1593 znew = num.empty(nx*ny, dtype=float)
1594 znew[:] = num.nan
1595 znew[zindi] = z
1596 return znew.reshape(ny, nx)
1599def guess_field_size(x_sorted, y_sorted, z=None, mode=None):
1600 critical_fraction = 1./num.e - 0.014*3
1601 xs = x_sorted
1602 ys = y_sorted
1603 nxs, nys = nvals(xs), nvals(ys)
1604 if mode == 'nonrandom':
1605 return nxs, nys, 0
1606 elif xs.size == nxs*nys:
1607 # exact match
1608 return nxs, nys, 0
1609 elif nxs >= xs.size*critical_fraction and nys >= xs.size*critical_fraction:
1610 # possibly randomly sampled
1611 nxs = int(math.sqrt(xs.size))
1612 nys = nxs
1613 return nxs, nys, 2
1614 else:
1615 return nxs, nys, 1
1618def griddata_auto(x, y, z, mode=None):
1619 '''
1620 Grid tabular XYZ data by binning.
1622 This function does some extra work to guess the size of the grid. This
1623 should work fine if the input values are already defined on an rectilinear
1624 grid, even if data points are missing or duplicated. This routine also
1625 tries to detect a random distribution of input data and in that case
1626 creates a grid of size sqrt(N) x sqrt(N).
1628 The points do not have to be given in any particular order. Grid nodes
1629 without data are assigned the NaN value. If multiple data points map to the
1630 same grid node, their average is assigned to the grid node.
1631 '''
1633 x, y, z = [num.asarray(X) for X in (x, y, z)]
1634 assert x.size == y.size == z.size
1635 xs, ys = num.sort(x), num.sort(y)
1636 nx, ny, badness = guess_field_size(xs, ys, z, mode=mode)
1637 if badness <= 1:
1638 xf = guess_vals(xs)
1639 yf = guess_vals(ys)
1640 zf = griddata_regular(x, y, z, xf, yf)
1641 else:
1642 xf = num.linspace(xs[0], xs[-1], nx)
1643 yf = num.linspace(ys[0], ys[-1], ny)
1644 zf = griddata_regular(x, y, z, xf, yf)
1646 return xf, yf, zf
1649def tabledata(xf, yf, zf):
1650 assert yf.size, xf.size == zf.shape
1651 x = num.tile(xf, yf.size)
1652 y = num.repeat(yf, xf.size)
1653 z = zf.flatten()
1654 return x, y, z
1657def double1d(a):
1658 a2 = num.empty(a.size*2-1)
1659 a2[::2] = a
1660 a2[1::2] = (a[:-1] + a[1:])/2.
1661 return a2
1664def double2d(f):
1665 f2 = num.empty((f.shape[0]*2-1, f.shape[1]*2-1))
1666 f2[:, :] = num.nan
1667 f2[::2, ::2] = f
1668 f2[1::2, ::2] = (f[:-1, :] + f[1:, :])/2.
1669 f2[::2, 1::2] = (f[:, :-1] + f[:, 1:])/2.
1670 f2[1::2, 1::2] = (f[:-1, :-1] + f[1:, :-1] + f[:-1, 1:] + f[1:, 1:])/4.
1671 diag = f2[1::2, 1::2]
1672 diagA = (f[:-1, :-1] + f[1:, 1:]) / 2.
1673 diagB = (f[1:, :-1] + f[:-1, 1:]) / 2.
1674 f2[1::2, 1::2] = num.where(num.isnan(diag), diagA, diag)
1675 f2[1::2, 1::2] = num.where(num.isnan(diag), diagB, diag)
1676 return f2
1679def doublegrid(x, y, z):
1680 x2 = double1d(x)
1681 y2 = double1d(y)
1682 z2 = double2d(z)
1683 return x2, y2, z2
1686class Guru(object):
1687 '''
1688 Abstract base class providing template interpolation, accessible as
1689 attributes.
1691 Classes deriving from this one, have to implement a :py:meth:`get_params`
1692 method, which is called to get a dict to do ordinary
1693 ``"%(key)x"``-substitutions. The deriving class must also provide a dict
1694 with the templates.
1695 '''
1697 def __init__(self):
1698 self.templates = {}
1700 def fill(self, templates, **kwargs):
1701 params = self.get_params(**kwargs)
1702 strings = [t % params for t in templates]
1703 return strings
1705 # hand through templates dict
1706 def __getitem__(self, template_name):
1707 return self.templates[template_name]
1709 def __setitem__(self, template_name, template):
1710 self.templates[template_name] = template
1712 def __contains__(self, template_name):
1713 return template_name in self.templates
1715 def __iter__(self):
1716 return iter(self.templates)
1718 def __len__(self):
1719 return len(self.templates)
1721 def __delitem__(self, template_name):
1722 del(self.templates[template_name])
1724 def _simple_fill(self, template_names, **kwargs):
1725 templates = [self.templates[n] for n in template_names]
1726 return self.fill(templates, **kwargs)
1728 def __getattr__(self, template_names):
1729 if [n for n in template_names if n not in self.templates]:
1730 raise AttributeError(template_names)
1732 def f(**kwargs):
1733 return self._simple_fill(template_names, **kwargs)
1735 return f
1738def nice_value(x):
1739 '''
1740 Round ``x`` to nice value.
1741 '''
1743 exp = 1.0
1744 sign = 1
1745 if x < 0.0:
1746 x = -x
1747 sign = -1
1748 while x >= 1.0:
1749 x /= 10.0
1750 exp *= 10.0
1751 while x < 0.1:
1752 x *= 10.0
1753 exp /= 10.0
1755 if x >= 0.75:
1756 return sign * 1.0 * exp
1757 if x >= 0.375:
1758 return sign * 0.5 * exp
1759 if x >= 0.225:
1760 return sign * 0.25 * exp
1761 if x >= 0.15:
1762 return sign * 0.2 * exp
1764 return sign * 0.1 * exp
1767class AutoScaler(object):
1768 '''
1769 Tunable 1D autoscaling based on data range.
1771 Instances of this class may be used to determine nice minima, maxima and
1772 increments for ax annotations, as well as suitable common exponents for
1773 notation.
1775 The autoscaling process is guided by the following public attributes:
1777 .. py:attribute:: approx_ticks
1779 Approximate number of increment steps (tickmarks) to generate.
1781 .. py:attribute:: mode
1783 Mode of operation: one of ``'auto'``, ``'min-max'``, ``'0-max'``,
1784 ``'min-0'``, ``'symmetric'`` or ``'off'``.
1786 ================ ==================================================
1787 mode description
1788 ================ ==================================================
1789 ``'auto'``: Look at data range and choose one of the choices
1790 below.
1791 ``'min-max'``: Output range is selected to include data range.
1792 ``'0-max'``: Output range shall start at zero and end at data
1793 max.
1794 ``'min-0'``: Output range shall start at data min and end at
1795 zero.
1796 ``'symmetric'``: Output range shall by symmetric by zero.
1797 ``'off'``: Similar to ``'min-max'``, but snap and space are
1798 disabled, such that the output range always
1799 exactly matches the data range.
1800 ================ ==================================================
1802 .. py:attribute:: exp
1804 If defined, override automatically determined exponent for notation
1805 by the given value.
1807 .. py:attribute:: snap
1809 If set to True, snap output range to multiples of increment. This
1810 parameter has no effect, if mode is set to ``'off'``.
1812 .. py:attribute:: inc
1814 If defined, override automatically determined tick increment by the
1815 given value.
1817 .. py:attribute:: space
1819 Add some padding to the range. The value given, is the fraction by
1820 which the output range is increased on each side. If mode is
1821 ``'0-max'`` or ``'min-0'``, the end at zero is kept fixed at zero.
1822 This parameter has no effect if mode is set to ``'off'``.
1824 .. py:attribute:: exp_factor
1826 Exponent of notation is chosen to be a multiple of this value.
1828 .. py:attribute:: no_exp_interval:
1830 Range of exponent, for which no exponential notation is allowed.
1832 '''
1834 def __init__(
1835 self,
1836 approx_ticks=7.0,
1837 mode='auto',
1838 exp=None,
1839 snap=False,
1840 inc=None,
1841 space=0.0,
1842 exp_factor=3,
1843 no_exp_interval=(-3, 5)):
1845 '''
1846 Create new AutoScaler instance.
1848 The parameters are described in the AutoScaler documentation.
1849 '''
1851 self.approx_ticks = approx_ticks
1852 self.mode = mode
1853 self.exp = exp
1854 self.snap = snap
1855 self.inc = inc
1856 self.space = space
1857 self.exp_factor = exp_factor
1858 self.no_exp_interval = no_exp_interval
1860 def make_scale(self, data_range, override_mode=None):
1862 '''
1863 Get nice minimum, maximum and increment for given data range.
1865 Returns ``(minimum, maximum, increment)`` or ``(maximum, minimum,
1866 -increment)``, depending on whether data_range is ``(data_min,
1867 data_max)`` or ``(data_max, data_min)``. If ``override_mode`` is
1868 defined, the mode attribute is temporarily overridden by the given
1869 value.
1870 '''
1872 data_min = min(data_range)
1873 data_max = max(data_range)
1875 is_reverse = (data_range[0] > data_range[1])
1877 a = self.mode
1878 if self.mode == 'auto':
1879 a = self.guess_autoscale_mode(data_min, data_max)
1881 if override_mode is not None:
1882 a = override_mode
1884 mi, ma = 0, 0
1885 if a == 'off':
1886 mi, ma = data_min, data_max
1887 elif a == '0-max':
1888 mi = 0.0
1889 if data_max > 0.0:
1890 ma = data_max
1891 else:
1892 ma = 1.0
1893 elif a == 'min-0':
1894 ma = 0.0
1895 if data_min < 0.0:
1896 mi = data_min
1897 else:
1898 mi = -1.0
1899 elif a == 'min-max':
1900 mi, ma = data_min, data_max
1901 elif a == 'symmetric':
1902 m = max(abs(data_min), abs(data_max))
1903 mi = -m
1904 ma = m
1906 nmi = mi
1907 if (mi != 0. or a == 'min-max') and a != 'off':
1908 nmi = mi - self.space*(ma-mi)
1910 nma = ma
1911 if (ma != 0. or a == 'min-max') and a != 'off':
1912 nma = ma + self.space*(ma-mi)
1914 mi, ma = nmi, nma
1916 if mi == ma and a != 'off':
1917 mi -= 1.0
1918 ma += 1.0
1920 # make nice tick increment
1921 if self.inc is not None:
1922 inc = self.inc
1923 else:
1924 if self.approx_ticks > 0.:
1925 inc = nice_value((ma-mi) / self.approx_ticks)
1926 else:
1927 inc = nice_value((ma-mi)*10.)
1929 if inc == 0.0:
1930 inc = 1.0
1932 # snap min and max to ticks if this is wanted
1933 if self.snap and a != 'off':
1934 ma = inc * math.ceil(ma/inc)
1935 mi = inc * math.floor(mi/inc)
1937 if is_reverse:
1938 return ma, mi, -inc
1939 else:
1940 return mi, ma, inc
1942 def make_exp(self, x):
1943 '''
1944 Get nice exponent for notation of ``x``.
1946 For ax annotations, give tick increment as ``x``.
1947 '''
1949 if self.exp is not None:
1950 return self.exp
1952 x = abs(x)
1953 if x == 0.0:
1954 return 0
1956 if 10**self.no_exp_interval[0] <= x <= 10**self.no_exp_interval[1]:
1957 return 0
1959 return math.floor(math.log10(x)/self.exp_factor)*self.exp_factor
1961 def guess_autoscale_mode(self, data_min, data_max):
1962 '''
1963 Guess mode of operation, based on data range.
1965 Used to map ``'auto'`` mode to ``'0-max'``, ``'min-0'``, ``'min-max'``
1966 or ``'symmetric'``.
1967 '''
1969 a = 'min-max'
1970 if data_min >= 0.0:
1971 if data_min < data_max/2.:
1972 a = '0-max'
1973 else:
1974 a = 'min-max'
1975 if data_max <= 0.0:
1976 if data_max > data_min/2.:
1977 a = 'min-0'
1978 else:
1979 a = 'min-max'
1980 if data_min < 0.0 and data_max > 0.0:
1981 if abs((abs(data_max)-abs(data_min)) /
1982 (abs(data_max)+abs(data_min))) < 0.5:
1983 a = 'symmetric'
1984 else:
1985 a = 'min-max'
1986 return a
1989class Ax(AutoScaler):
1990 '''
1991 Ax description with autoscaling capabilities.
1993 The ax is described by the :py:class:`AutoScaler` public attributes, plus
1994 the following additional attributes (with default values given in
1995 paranthesis):
1997 .. py:attribute:: label
1999 Ax label (without unit).
2001 .. py:attribute:: unit
2003 Physical unit of the data attached to this ax.
2005 .. py:attribute:: scaled_unit
2007 (see below)
2009 .. py:attribute:: scaled_unit_factor
2011 Scaled physical unit and factor between unit and scaled_unit so that
2013 unit = scaled_unit_factor x scaled_unit.
2015 (E.g. if unit is 'm' and data is in the range of nanometers, you may
2016 want to set the scaled_unit to 'nm' and the scaled_unit_factor to
2017 1e9.)
2019 .. py:attribute:: limits
2021 If defined, fix range of ax to limits=(min,max).
2023 .. py:attribute:: masking
2025 If true and if there is a limit on the ax, while calculating ranges,
2026 the data points are masked such that data points outside of this axes
2027 limits are not used to determine the range of another dependant ax.
2029 '''
2031 def __init__(self, label='', unit='', scaled_unit_factor=1.,
2032 scaled_unit='', limits=None, masking=True, **kwargs):
2034 AutoScaler.__init__(self, **kwargs)
2035 self.label = label
2036 self.unit = unit
2037 self.scaled_unit_factor = scaled_unit_factor
2038 self.scaled_unit = scaled_unit
2039 self.limits = limits
2040 self.masking = masking
2042 def label_str(self, exp, unit):
2043 '''
2044 Get label string including the unit and multiplier.
2045 '''
2047 slabel, sunit, sexp = '', '', ''
2048 if self.label:
2049 slabel = self.label
2051 if unit or exp != 0:
2052 if exp != 0:
2053 sexp = '\\327 10@+%i@+' % exp
2054 sunit = '[ %s %s ]' % (sexp, unit)
2055 else:
2056 sunit = '[ %s ]' % unit
2058 p = []
2059 if slabel:
2060 p.append(slabel)
2062 if sunit:
2063 p.append(sunit)
2065 return ' '.join(p)
2067 def make_params(self, data_range, ax_projection=False, override_mode=None,
2068 override_scaled_unit_factor=None):
2070 '''
2071 Get minimum, maximum, increment and label string for ax display.'
2073 Returns minimum, maximum, increment and label string including unit and
2074 multiplier for given data range.
2076 If ``ax_projection`` is True, values suitable to be displayed on the ax
2077 are returned, e.g. min, max and inc are returned in scaled units.
2078 Otherwise the values are returned in the original units, without any
2079 scaling applied.
2080 '''
2082 sf = self.scaled_unit_factor
2084 if override_scaled_unit_factor is not None:
2085 sf = override_scaled_unit_factor
2087 dr_scaled = [sf*x for x in data_range]
2089 mi, ma, inc = self.make_scale(dr_scaled, override_mode=override_mode)
2090 if self.inc is not None:
2091 inc = self.inc*sf
2093 if ax_projection:
2094 exp = self.make_exp(inc)
2095 if sf == 1. and override_scaled_unit_factor is None:
2096 unit = self.unit
2097 else:
2098 unit = self.scaled_unit
2099 label = self.label_str(exp, unit)
2100 return mi/10**exp, ma/10**exp, inc/10**exp, label
2101 else:
2102 label = self.label_str(0, self.unit)
2103 return mi/sf, ma/sf, inc/sf, label
2106class ScaleGuru(Guru):
2108 '''
2109 2D/3D autoscaling and ax annotation facility.
2111 Instances of this class provide automatic determination of plot ranges,
2112 tick increments and scaled annotations, as well as label/unit handling. It
2113 can in particular be used to automatically generate the -R and -B option
2114 arguments, which are required for most GMT commands.
2116 It extends the functionality of the :py:class:`Ax` and
2117 :py:class:`AutoScaler` classes at the level, where it can not be handled
2118 anymore by looking at a single dimension of the dataset's data, e.g.:
2120 * The ability to impose a fixed aspect ratio between two axes.
2122 * Recalculation of data range on non-limited axes, when there are
2123 limits imposed on other axes.
2125 '''
2127 def __init__(self, data_tuples=None, axes=None, aspect=None,
2128 percent_interval=None, copy_from=None):
2130 Guru.__init__(self)
2132 if copy_from:
2133 self.templates = copy.deepcopy(copy_from.templates)
2134 self.axes = copy.deepcopy(copy_from.axes)
2135 self.data_ranges = copy.deepcopy(copy_from.data_ranges)
2136 self.aspect = copy_from.aspect
2138 if percent_interval is not None:
2139 from scipy.stats import scoreatpercentile as scap
2141 self.templates = dict(
2142 R='-R%(xmin)g/%(xmax)g/%(ymin)g/%(ymax)g',
2143 B='-B%(xinc)g:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen',
2144 T='-T%(zmin)g/%(zmax)g/%(zinc)g')
2146 maxdim = 2
2147 if data_tuples:
2148 maxdim = max(maxdim, max([len(dt) for dt in data_tuples]))
2149 else:
2150 if axes:
2151 maxdim = len(axes)
2152 data_tuples = [([],) * maxdim]
2153 if axes is not None:
2154 self.axes = axes
2155 else:
2156 self.axes = [Ax() for i in range(maxdim)]
2158 # sophisticated data-range calculation
2159 data_ranges = [None] * maxdim
2160 for dt_ in data_tuples:
2161 dt = num.asarray(dt_)
2162 in_range = True
2163 for ax, x in zip(self.axes, dt):
2164 if ax.limits and ax.masking:
2165 ax_limits = list(ax.limits)
2166 if ax_limits[0] is None:
2167 ax_limits[0] = -num.inf
2168 if ax_limits[1] is None:
2169 ax_limits[1] = num.inf
2170 in_range = num.logical_and(
2171 in_range,
2172 num.logical_and(ax_limits[0] <= x, x <= ax_limits[1]))
2174 for i, ax, x in zip(range(maxdim), self.axes, dt):
2176 if not ax.limits or None in ax.limits:
2177 if len(x) >= 1:
2178 if in_range is not True:
2179 xmasked = num.where(in_range, x, num.NaN)
2180 if percent_interval is None:
2181 range_this = (
2182 num.nanmin(xmasked),
2183 num.nanmax(xmasked))
2184 else:
2185 xmasked_finite = num.compress(
2186 num.isfinite(xmasked), xmasked)
2187 range_this = (
2188 scap(xmasked_finite,
2189 (100.-percent_interval)/2.),
2190 scap(xmasked_finite,
2191 100.-(100.-percent_interval)/2.))
2192 else:
2193 if percent_interval is None:
2194 range_this = num.nanmin(x), num.nanmax(x)
2195 else:
2196 xmasked_finite = num.compress(
2197 num.isfinite(xmasked), xmasked)
2198 range_this = (
2199 scap(xmasked_finite,
2200 (100.-percent_interval)/2.),
2201 scap(xmasked_finite,
2202 100.-(100.-percent_interval)/2.))
2203 else:
2204 range_this = (0., 1.)
2206 if ax.limits:
2207 if ax.limits[0] is not None:
2208 range_this = ax.limits[0], max(ax.limits[0],
2209 range_this[1])
2211 if ax.limits[1] is not None:
2212 range_this = min(ax.limits[1],
2213 range_this[0]), ax.limits[1]
2215 else:
2216 range_this = ax.limits
2218 if data_ranges[i] is None and range_this[0] <= range_this[1]:
2219 data_ranges[i] = range_this
2220 else:
2221 mi, ma = range_this
2222 if data_ranges[i] is not None:
2223 mi = min(data_ranges[i][0], mi)
2224 ma = max(data_ranges[i][1], ma)
2226 data_ranges[i] = (mi, ma)
2228 for i in range(len(data_ranges)):
2229 if data_ranges[i] is None or not (
2230 num.isfinite(data_ranges[i][0])
2231 and num.isfinite(data_ranges[i][1])):
2233 data_ranges[i] = (0., 1.)
2235 self.data_ranges = data_ranges
2236 self.aspect = aspect
2238 def copy(self):
2239 return ScaleGuru(copy_from=self)
2241 def get_params(self, ax_projection=False):
2243 '''
2244 Get dict with output parameters.
2246 For each data dimension, ax minimum, maximum, increment and a label
2247 string (including unit and exponential factor) are determined. E.g. in
2248 for the first dimension the output dict will contain the keys
2249 ``'xmin'``, ``'xmax'``, ``'xinc'``, and ``'xlabel'``.
2251 Normally, values corresponding to the scaling of the raw data are
2252 produced, but if ``ax_projection`` is ``True``, values which are
2253 suitable to be printed on the axes are returned. This means that in the
2254 latter case, the :py:attr:`Ax.scaled_unit` and
2255 :py:attr:`Ax.scaled_unit_factor` attributes as set on the axes are
2256 respected and that a common 10^x factor is factored out and put to the
2257 label string.
2258 '''
2260 xmi, xma, xinc, xlabel = self.axes[0].make_params(
2261 self.data_ranges[0], ax_projection)
2262 ymi, yma, yinc, ylabel = self.axes[1].make_params(
2263 self.data_ranges[1], ax_projection)
2264 if len(self.axes) > 2:
2265 zmi, zma, zinc, zlabel = self.axes[2].make_params(
2266 self.data_ranges[2], ax_projection)
2268 # enforce certain aspect, if needed
2269 if self.aspect is not None:
2270 xwid = xma-xmi
2271 ywid = yma-ymi
2272 if ywid < xwid*self.aspect:
2273 ymi -= (xwid*self.aspect - ywid)*0.5
2274 yma += (xwid*self.aspect - ywid)*0.5
2275 ymi, yma, yinc, ylabel = self.axes[1].make_params(
2276 (ymi, yma), ax_projection, override_mode='off',
2277 override_scaled_unit_factor=1.)
2279 elif xwid < ywid/self.aspect:
2280 xmi -= (ywid/self.aspect - xwid)*0.5
2281 xma += (ywid/self.aspect - xwid)*0.5
2282 xmi, xma, xinc, xlabel = self.axes[0].make_params(
2283 (xmi, xma), ax_projection, override_mode='off',
2284 override_scaled_unit_factor=1.)
2286 params = dict(xmin=xmi, xmax=xma, xinc=xinc, xlabel=xlabel,
2287 ymin=ymi, ymax=yma, yinc=yinc, ylabel=ylabel)
2288 if len(self.axes) > 2:
2289 params.update(dict(zmin=zmi, zmax=zma, zinc=zinc, zlabel=zlabel))
2291 return params
2294class GumSpring(object):
2296 '''
2297 Sizing policy implementing a minimal size, plus a desire to grow.
2298 '''
2300 def __init__(self, minimal=None, grow=None):
2301 self.minimal = minimal
2302 if grow is None:
2303 if minimal is None:
2304 self.grow = 1.0
2305 else:
2306 self.grow = 0.0
2307 else:
2308 self.grow = grow
2309 self.value = 1.0
2311 def get_minimal(self):
2312 if self.minimal is not None:
2313 return self.minimal
2314 else:
2315 return 0.0
2317 def get_grow(self):
2318 return self.grow
2320 def set_value(self, value):
2321 self.value = value
2323 def get_value(self):
2324 return self.value
2327def distribute(sizes, grows, space):
2328 sizes = list(sizes)
2329 gsum = sum(grows)
2330 if gsum > 0.0:
2331 for i in range(len(sizes)):
2332 sizes[i] += space*grows[i]/gsum
2333 return sizes
2336class Widget(Guru):
2338 '''
2339 Base class of the gmtpy layout system.
2341 The Widget class provides the basic functionality for the nesting and
2342 placing of elements on the output page, and maintains the sizing policies
2343 of each element. Each of the layouts defined in gmtpy is itself a Widget.
2345 Sizing of the widget is controlled by :py:meth:`get_min_size` and
2346 :py:meth:`get_grow` which should be overloaded in derived classes. The
2347 basic behaviour of a Widget instance is to have a vertical and a horizontal
2348 minimum size which default to zero, as well as a vertical and a horizontal
2349 desire to grow, represented by floats, which default to 1.0. Additionally
2350 an aspect ratio constraint may be imposed on the Widget.
2352 After layouting, the widget provides its width, height, x-offset and
2353 y-offset in various ways. Via the Guru interface (see :py:class:`Guru`
2354 class), templates for the -X, -Y and -J option arguments used by GMT
2355 arguments are provided. The defaults are suitable for plotting of linear
2356 (-JX) plots. Other projections can be selected by giving an appropriate 'J'
2357 template, or by manual construction of the -J option, e.g. by utilizing the
2358 :py:meth:`width` and :py:meth:`height` methods. The :py:meth:`bbox` method
2359 can be used to create a PostScript bounding box from the widgets border,
2360 e.g. for use in the :py:meth:`save` method of :py:class:`GMT` instances.
2362 The convention is, that all sizes are given in PostScript points.
2363 Conversion factors are provided as constants :py:const:`inch` and
2364 :py:const:`cm` in the gmtpy module.
2365 '''
2367 def __init__(self, horizontal=None, vertical=None, parent=None):
2369 '''
2370 Create new widget.
2371 '''
2373 Guru.__init__(self)
2375 self.templates = dict(
2376 X='-Xa%(xoffset)gp',
2377 Y='-Ya%(yoffset)gp',
2378 J='-JX%(width)gp/%(height)gp')
2380 if horizontal is None:
2381 self.horizontal = GumSpring()
2382 else:
2383 self.horizontal = horizontal
2385 if vertical is None:
2386 self.vertical = GumSpring()
2387 else:
2388 self.vertical = vertical
2390 self.aspect = None
2391 self.parent = parent
2392 self.dirty = True
2394 def set_parent(self, parent):
2396 '''
2397 Set the parent widget.
2399 This method should not be called directly. The :py:meth:`set_widget`
2400 methods are responsible for calling this.
2401 '''
2403 self.parent = parent
2404 self.dirtyfy()
2406 def get_parent(self):
2408 '''
2409 Get the widgets parent widget.
2410 '''
2412 return self.parent
2414 def get_root(self):
2416 '''
2417 Get the root widget in the layout hierarchy.
2418 '''
2420 if self.parent is not None:
2421 return self.get_parent()
2422 else:
2423 return self
2425 def set_horizontal(self, minimal=None, grow=None):
2427 '''
2428 Set the horizontal sizing policy of the Widget.
2431 :param minimal: new minimal width of the widget
2432 :param grow: new horizontal grow disire of the widget
2433 '''
2435 self.horizontal = GumSpring(minimal, grow)
2436 self.dirtyfy()
2438 def get_horizontal(self):
2439 return self.horizontal.get_minimal(), self.horizontal.get_grow()
2441 def set_vertical(self, minimal=None, grow=None):
2443 '''
2444 Set the horizontal sizing policy of the Widget.
2446 :param minimal: new minimal height of the widget
2447 :param grow: new vertical grow disire of the widget
2448 '''
2450 self.vertical = GumSpring(minimal, grow)
2451 self.dirtyfy()
2453 def get_vertical(self):
2454 return self.vertical.get_minimal(), self.vertical.get_grow()
2456 def set_aspect(self, aspect=None):
2458 '''
2459 Set aspect constraint on the widget.
2461 The aspect is given as height divided by width.
2462 '''
2464 self.aspect = aspect
2465 self.dirtyfy()
2467 def set_policy(self, minimal=(None, None), grow=(None, None), aspect=None):
2469 '''
2470 Shortcut to set sizing and aspect constraints in a single method
2471 call.
2472 '''
2474 self.set_horizontal(minimal[0], grow[0])
2475 self.set_vertical(minimal[1], grow[1])
2476 self.set_aspect(aspect)
2478 def get_policy(self):
2479 mh, gh = self.get_horizontal()
2480 mv, gv = self.get_vertical()
2481 return (mh, mv), (gh, gv), self.aspect
2483 def legalize(self, size, offset):
2485 '''
2486 Get legal size for widget.
2488 Returns: (new_size, new_offset)
2490 Given a box as ``size`` and ``offset``, return ``new_size`` and
2491 ``new_offset``, such that the widget's sizing and aspect constraints
2492 are fullfilled. The returned box is centered on the given input box.
2493 '''
2495 sh, sv = size
2496 oh, ov = offset
2497 shs, svs = Widget.get_min_size(self)
2498 ghs, gvs = Widget.get_grow(self)
2500 if ghs == 0.0:
2501 oh += (sh-shs)/2.
2502 sh = shs
2504 if gvs == 0.0:
2505 ov += (sv-svs)/2.
2506 sv = svs
2508 if self.aspect is not None:
2509 if sh > sv/self.aspect:
2510 oh += (sh-sv/self.aspect)/2.
2511 sh = sv/self.aspect
2512 if sv > sh*self.aspect:
2513 ov += (sv-sh*self.aspect)/2.
2514 sv = sh*self.aspect
2516 return (sh, sv), (oh, ov)
2518 def get_min_size(self):
2520 '''
2521 Get minimum size of widget.
2523 Used by the layout managers. Should be overloaded in derived classes.
2524 '''
2526 mh, mv = self.horizontal.get_minimal(), self.vertical.get_minimal()
2527 if self.aspect is not None:
2528 if mv == 0.0:
2529 return mh, mh*self.aspect
2530 elif mh == 0.0:
2531 return mv/self.aspect, mv
2532 return mh, mv
2534 def get_grow(self):
2536 '''
2537 Get widget's desire to grow.
2539 Used by the layout managers. Should be overloaded in derived classes.
2540 '''
2542 return self.horizontal.get_grow(), self.vertical.get_grow()
2544 def set_size(self, size, offset):
2546 '''
2547 Set the widget's current size.
2549 Should not be called directly. It is the layout manager's
2550 responsibility to call this.
2551 '''
2553 (sh, sv), inner_offset = self.legalize(size, offset)
2554 self.offset = inner_offset
2555 self.horizontal.set_value(sh)
2556 self.vertical.set_value(sv)
2557 self.dirty = False
2559 def __str__(self):
2561 def indent(ind, str):
2562 return ('\n'+ind).join(str.splitlines())
2563 size, offset = self.get_size()
2564 s = "%s (%g x %g) (%g, %g)\n" % ((self.__class__,) + size + offset)
2565 children = self.get_children()
2566 if children:
2567 s += '\n'.join([' ' + indent(' ', str(c)) for c in children])
2568 return s
2570 def policies_debug_str(self):
2572 def indent(ind, str):
2573 return ('\n'+ind).join(str.splitlines())
2574 mins, grows, aspect = self.get_policy()
2575 s = "%s: minimum=(%s, %s), grow=(%s, %s), aspect=%s\n" % (
2576 (self.__class__,) + mins+grows+(aspect,))
2578 children = self.get_children()
2579 if children:
2580 s += '\n'.join([' ' + indent(
2581 ' ', c.policies_debug_str()) for c in children])
2582 return s
2584 def get_corners(self, descend=False):
2586 '''
2587 Get coordinates of the corners of the widget.
2589 Returns list with coordinate tuples.
2591 If ``descend`` is True, the returned list will contain corner
2592 coordinates of all sub-widgets.
2593 '''
2595 self.do_layout()
2596 (sh, sv), (oh, ov) = self.get_size()
2597 corners = [(oh, ov), (oh+sh, ov), (oh+sh, ov+sv), (oh, ov+sv)]
2598 if descend:
2599 for child in self.get_children():
2600 corners.extend(child.get_corners(descend=True))
2601 return corners
2603 def get_sizes(self):
2605 '''
2606 Get sizes of this widget and all it's children.
2608 Returns a list with size tuples.
2609 '''
2610 self.do_layout()
2611 sizes = [self.get_size()]
2612 for child in self.get_children():
2613 sizes.extend(child.get_sizes())
2614 return sizes
2616 def do_layout(self):
2618 '''
2619 Triggers layouting of the widget hierarchy, if needed.
2620 '''
2622 if self.parent is not None:
2623 return self.parent.do_layout()
2625 if not self.dirty:
2626 return
2628 sh, sv = self.get_min_size()
2629 gh, gv = self.get_grow()
2630 if sh == 0.0 and gh != 0.0:
2631 sh = 15.*cm
2632 if sv == 0.0 and gv != 0.0:
2633 sv = 15.*cm*gv/gh * 1./golden_ratio
2634 self.set_size((sh, sv), (0., 0.))
2636 def get_children(self):
2638 '''
2639 Get sub-widgets contained in this widget.
2641 Returns a list of widgets.
2642 '''
2644 return []
2646 def get_size(self):
2648 '''
2649 Get current size and position of the widget.
2651 Triggers layouting and returns
2652 ``((width, height), (xoffset, yoffset))``
2653 '''
2655 self.do_layout()
2656 return (self.horizontal.get_value(),
2657 self.vertical.get_value()), self.offset
2659 def get_params(self):
2661 '''
2662 Get current size and position of the widget.
2664 Triggers layouting and returns dict with keys ``'xoffset'``,
2665 ``'yoffset'``, ``'width'`` and ``'height'``.
2666 '''
2668 self.do_layout()
2669 (w, h), (xo, yo) = self.get_size()
2670 return dict(xoffset=xo, yoffset=yo, width=w, height=h,
2671 width_m=w/_units['m'])
2673 def width(self):
2675 '''
2676 Get current width of the widget.
2678 Triggers layouting and returns width.
2679 '''
2681 self.do_layout()
2682 return self.horizontal.get_value()
2684 def height(self):
2686 '''
2687 Get current height of the widget.
2689 Triggers layouting and return height.
2690 '''
2692 self.do_layout()
2693 return self.vertical.get_value()
2695 def bbox(self):
2697 '''
2698 Get PostScript bounding box for this widget.
2700 Triggers layouting and returns values suitable to create PS bounding
2701 box, representing the widgets current size and position.
2702 '''
2704 self.do_layout()
2705 return (self.offset[0], self.offset[1], self.offset[0]+self.width(),
2706 self.offset[1]+self.height())
2708 def dirtyfy(self):
2710 '''
2711 Set dirty flag on top level widget in the hierarchy.
2713 Called by various methods, to indicate, that the widget hierarchy needs
2714 new layouting.
2715 '''
2717 if self.parent is not None:
2718 self.parent.dirtyfy()
2720 self.dirty = True
2723class CenterLayout(Widget):
2725 '''
2726 A layout manager which centers its single child widget.
2728 The child widget may be oversized.
2729 '''
2731 def __init__(self, horizontal=None, vertical=None):
2732 Widget.__init__(self, horizontal, vertical)
2733 self.content = Widget(horizontal=GumSpring(grow=1.),
2734 vertical=GumSpring(grow=1.), parent=self)
2736 def get_min_size(self):
2737 shs, svs = Widget.get_min_size(self)
2738 sh, sv = self.content.get_min_size()
2739 return max(shs, sh), max(svs, sv)
2741 def get_grow(self):
2742 ghs, gvs = Widget.get_grow(self)
2743 gh, gv = self.content.get_grow()
2744 return gh*ghs, gv*gvs
2746 def set_size(self, size, offset):
2747 (sh, sv), (oh, ov) = self.legalize(size, offset)
2749 shc, svc = self.content.get_min_size()
2750 ghc, gvc = self.content.get_grow()
2751 if ghc != 0.:
2752 shc = sh
2753 if gvc != 0.:
2754 svc = sv
2755 ohc = oh+(sh-shc)/2.
2756 ovc = ov+(sv-svc)/2.
2758 self.content.set_size((shc, svc), (ohc, ovc))
2759 Widget.set_size(self, (sh, sv), (oh, ov))
2761 def set_widget(self, widget=None):
2763 '''
2764 Set the child widget, which shall be centered.
2765 '''
2767 if widget is None:
2768 widget = Widget()
2770 self.content = widget
2772 widget.set_parent(self)
2774 def get_widget(self):
2775 return self.content
2777 def get_children(self):
2778 return [self.content]
2781class FrameLayout(Widget):
2783 '''
2784 A layout manager containing a center widget sorrounded by four margin
2785 widgets.
2787 ::
2789 +---------------------------+
2790 | top |
2791 +---------------------------+
2792 | | | |
2793 | left | center | right |
2794 | | | |
2795 +---------------------------+
2796 | bottom |
2797 +---------------------------+
2799 This layout manager does a little bit of extra effort to maintain the
2800 aspect constraint of the center widget, if this is set. It does so, by
2801 allowing for a bit more flexibility in the sizing of the margins. Two
2802 shortcut methods are provided to set the margin sizes in one shot:
2803 :py:meth:`set_fixed_margins` and :py:meth:`set_min_margins`. The first sets
2804 the margins to fixed sizes, while the second gives them a minimal size and
2805 a (neglectably) small desire to grow. Using the latter may be useful when
2806 setting an aspect constraint on the center widget, because this way the
2807 maximum size of the center widget may be controlled without creating empty
2808 spaces between the widgets.
2809 '''
2811 def __init__(self, horizontal=None, vertical=None):
2812 Widget.__init__(self, horizontal, vertical)
2813 mw = 3.*cm
2814 self.left = Widget(
2815 horizontal=GumSpring(grow=0.15, minimal=mw), parent=self)
2816 self.right = Widget(
2817 horizontal=GumSpring(grow=0.15, minimal=mw), parent=self)
2818 self.top = Widget(
2819 vertical=GumSpring(grow=0.15, minimal=mw/golden_ratio),
2820 parent=self)
2821 self.bottom = Widget(
2822 vertical=GumSpring(grow=0.15, minimal=mw/golden_ratio),
2823 parent=self)
2824 self.center = Widget(
2825 horizontal=GumSpring(grow=0.7), vertical=GumSpring(grow=0.7),
2826 parent=self)
2828 def set_fixed_margins(self, left, right, top, bottom):
2829 '''
2830 Give margins fixed size constraints.
2831 '''
2833 self.left.set_horizontal(left, 0)
2834 self.right.set_horizontal(right, 0)
2835 self.top.set_vertical(top, 0)
2836 self.bottom.set_vertical(bottom, 0)
2838 def set_min_margins(self, left, right, top, bottom, grow=0.0001):
2839 '''
2840 Give margins a minimal size and the possibility to grow.
2842 The desire to grow is set to a very small number.
2843 '''
2844 self.left.set_horizontal(left, grow)
2845 self.right.set_horizontal(right, grow)
2846 self.top.set_vertical(top, grow)
2847 self.bottom.set_vertical(bottom, grow)
2849 def get_min_size(self):
2850 shs, svs = Widget.get_min_size(self)
2852 sl, sr, st, sb, sc = [x.get_min_size() for x in (
2853 self.left, self.right, self.top, self.bottom, self.center)]
2854 gl, gr, gt, gb, gc = [x.get_grow() for x in (
2855 self.left, self.right, self.top, self.bottom, self.center)]
2857 shsum = sl[0]+sr[0]+sc[0]
2858 svsum = st[1]+sb[1]+sc[1]
2860 # prevent widgets from collapsing
2861 for s, g in ((sl, gl), (sr, gr), (sc, gc)):
2862 if s[0] == 0.0 and g[0] != 0.0:
2863 shsum += 0.1*cm
2865 for s, g in ((st, gt), (sb, gb), (sc, gc)):
2866 if s[1] == 0.0 and g[1] != 0.0:
2867 svsum += 0.1*cm
2869 sh = max(shs, shsum)
2870 sv = max(svs, svsum)
2872 return sh, sv
2874 def get_grow(self):
2875 ghs, gvs = Widget.get_grow(self)
2876 gh = (self.left.get_grow()[0] +
2877 self.right.get_grow()[0] +
2878 self.center.get_grow()[0]) * ghs
2879 gv = (self.top.get_grow()[1] +
2880 self.bottom.get_grow()[1] +
2881 self.center.get_grow()[1]) * gvs
2882 return gh, gv
2884 def set_size(self, size, offset):
2885 (sh, sv), (oh, ov) = self.legalize(size, offset)
2887 sl, sr, st, sb, sc = [x.get_min_size() for x in (
2888 self.left, self.right, self.top, self.bottom, self.center)]
2889 gl, gr, gt, gb, gc = [x.get_grow() for x in (
2890 self.left, self.right, self.top, self.bottom, self.center)]
2892 ah = sh - (sl[0]+sr[0]+sc[0])
2893 av = sv - (st[1]+sb[1]+sc[1])
2895 if ah < 0.0:
2896 raise GmtPyError("Container not wide enough for contents "
2897 "(FrameLayout, available: %g cm, needed: %g cm)"
2898 % (sh/cm, (sl[0]+sr[0]+sc[0])/cm))
2899 if av < 0.0:
2900 raise GmtPyError("Container not high enough for contents "
2901 "(FrameLayout, available: %g cm, needed: %g cm)"
2902 % (sv/cm, (st[1]+sb[1]+sc[1])/cm))
2904 slh, srh, sch = distribute((sl[0], sr[0], sc[0]),
2905 (gl[0], gr[0], gc[0]), ah)
2906 stv, sbv, scv = distribute((st[1], sb[1], sc[1]),
2907 (gt[1], gb[1], gc[1]), av)
2909 if self.center.aspect is not None:
2910 ahm = sh - (sl[0]+sr[0] + scv/self.center.aspect)
2911 avm = sv - (st[1]+sb[1] + sch*self.center.aspect)
2912 if 0.0 < ahm < ah:
2913 slh, srh, sch = distribute(
2914 (sl[0], sr[0], scv/self.center.aspect),
2915 (gl[0], gr[0], 0.0), ahm)
2917 elif 0.0 < avm < av:
2918 stv, sbv, scv = distribute((st[1], sb[1],
2919 sch*self.center.aspect),
2920 (gt[1], gb[1], 0.0), avm)
2922 ah = sh - (slh+srh+sch)
2923 av = sv - (stv+sbv+scv)
2925 oh += ah/2.
2926 ov += av/2.
2927 sh -= ah
2928 sv -= av
2930 self.left.set_size((slh, scv), (oh, ov+sbv))
2931 self.right.set_size((srh, scv), (oh+slh+sch, ov+sbv))
2932 self.top.set_size((sh, stv), (oh, ov+sbv+scv))
2933 self.bottom.set_size((sh, sbv), (oh, ov))
2934 self.center.set_size((sch, scv), (oh+slh, ov+sbv))
2935 Widget.set_size(self, (sh, sv), (oh, ov))
2937 def set_widget(self, which='center', widget=None):
2939 '''
2940 Set one of the sub-widgets.
2942 ``which`` should be one of ``'left'``, ``'right'``, ``'top'``,
2943 ``'bottom'`` or ``'center'``.
2944 '''
2946 if widget is None:
2947 widget = Widget()
2949 if which in ('left', 'right', 'top', 'bottom', 'center'):
2950 self.__dict__[which] = widget
2951 else:
2952 raise GmtPyError('No such sub-widget: %s' % which)
2954 widget.set_parent(self)
2956 def get_widget(self, which='center'):
2958 '''
2959 Get one of the sub-widgets.
2961 ``which`` should be one of ``'left'``, ``'right'``, ``'top'``,
2962 ``'bottom'`` or ``'center'``.
2963 '''
2965 if which in ('left', 'right', 'top', 'bottom', 'center'):
2966 return self.__dict__[which]
2967 else:
2968 raise GmtPyError('No such sub-widget: %s' % which)
2970 def get_children(self):
2971 return [self.left, self.right, self.top, self.bottom, self.center]
2974class GridLayout(Widget):
2976 '''
2977 A layout manager which arranges its sub-widgets in a grid.
2979 The grid spacing is flexible and based on the sizing policies of the
2980 contained sub-widgets. If an equidistant grid is needed, the sizing
2981 policies of the sub-widgets have to be set equally.
2983 The height of each row and the width of each column is derived from the
2984 sizing policy of the largest sub-widget in the row or column in question.
2985 The algorithm is not very sophisticated, so conflicting sizing policies
2986 might not be resolved optimally.
2987 '''
2989 def __init__(self, nx=2, ny=2, horizontal=None, vertical=None):
2991 '''
2992 Create new grid layout with ``nx`` columns and ``ny`` rows.
2993 '''
2995 Widget.__init__(self, horizontal, vertical)
2996 self.grid = []
2997 for iy in range(ny):
2998 row = []
2999 for ix in range(nx):
3000 w = Widget(parent=self)
3001 row.append(w)
3003 self.grid.append(row)
3005 def sub_min_sizes_as_array(self):
3006 esh = num.array(
3007 [[w.get_min_size()[0] for w in row] for row in self.grid],
3008 dtype=float)
3009 esv = num.array(
3010 [[w.get_min_size()[1] for w in row] for row in self.grid],
3011 dtype=float)
3012 return esh, esv
3014 def sub_grows_as_array(self):
3015 egh = num.array(
3016 [[w.get_grow()[0] for w in row] for row in self.grid],
3017 dtype=float)
3018 egv = num.array(
3019 [[w.get_grow()[1] for w in row] for row in self.grid],
3020 dtype=float)
3021 return egh, egv
3023 def get_min_size(self):
3024 sh, sv = Widget.get_min_size(self)
3025 esh, esv = self.sub_min_sizes_as_array()
3026 if esh.size != 0:
3027 sh = max(sh, num.sum(esh.max(0)))
3028 if esv.size != 0:
3029 sv = max(sv, num.sum(esv.max(1)))
3030 return sh, sv
3032 def get_grow(self):
3033 ghs, gvs = Widget.get_grow(self)
3034 egh, egv = self.sub_grows_as_array()
3035 if egh.size != 0:
3036 gh = num.sum(egh.max(0))*ghs
3037 else:
3038 gh = 1.0
3039 if egv.size != 0:
3040 gv = num.sum(egv.max(1))*gvs
3041 else:
3042 gv = 1.0
3043 return gh, gv
3045 def set_size(self, size, offset):
3046 (sh, sv), (oh, ov) = self.legalize(size, offset)
3047 esh, esv = self.sub_min_sizes_as_array()
3048 egh, egv = self.sub_grows_as_array()
3050 # available additional space
3051 empty = esh.size == 0
3053 if not empty:
3054 ah = sh - num.sum(esh.max(0))
3055 av = sv - num.sum(esv.max(1))
3056 else:
3057 av = sv
3058 ah = sh
3060 if ah < 0.0:
3061 raise GmtPyError("Container not wide enough for contents "
3062 "(GridLayout, available: %g cm, needed: %g cm)"
3063 % (sh/cm, (num.sum(esh.max(0)))/cm))
3064 if av < 0.0:
3065 raise GmtPyError("Container not high enough for contents "
3066 "(GridLayout, available: %g cm, needed: %g cm)"
3067 % (sv/cm, (num.sum(esv.max(1)))/cm))
3069 nx, ny = esh.shape
3071 if not empty:
3072 # distribute additional space on rows and columns
3073 # according to grow weights and minimal sizes
3074 gsh = egh.sum(1)[:, num.newaxis].repeat(ny, axis=1)
3075 nesh = esh.copy()
3076 nesh += num.where(gsh > 0.0, ah*egh/gsh, 0.0)
3078 nsh = num.maximum(nesh.max(0), esh.max(0))
3080 gsv = egv.sum(0)[num.newaxis, :].repeat(nx, axis=0)
3081 nesv = esv.copy()
3082 nesv += num.where(gsv > 0.0, av*egv/gsv, 0.0)
3083 nsv = num.maximum(nesv.max(1), esv.max(1))
3085 ah = sh - sum(nsh)
3086 av = sv - sum(nsv)
3088 oh += ah/2.
3089 ov += av/2.
3090 sh -= ah
3091 sv -= av
3093 # resize child widgets
3094 neov = ov + sum(nsv)
3095 for row, nesv in zip(self.grid, nsv):
3096 neov -= nesv
3097 neoh = oh
3098 for w, nesh in zip(row, nsh):
3099 w.set_size((nesh, nesv), (neoh, neov))
3100 neoh += nesh
3102 Widget.set_size(self, (sh, sv), (oh, ov))
3104 def set_widget(self, ix, iy, widget=None):
3106 '''
3107 Set one of the sub-widgets.
3109 Sets the sub-widget in column ``ix`` and row ``iy``. The indices are
3110 counted from zero.
3111 '''
3113 if widget is None:
3114 widget = Widget()
3116 self.grid[iy][ix] = widget
3117 widget.set_parent(self)
3119 def get_widget(self, ix, iy):
3121 '''
3122 Get one of the sub-widgets.
3124 Gets the sub-widget from column ``ix`` and row ``iy``. The indices are
3125 counted from zero.
3126 '''
3128 return self.grid[iy][ix]
3130 def get_children(self):
3131 children = []
3132 for row in self.grid:
3133 children.extend(row)
3135 return children
3138def is_gmt5(version='newest'):
3139 return get_gmt_installation(version)['version'][0] == '5'
3142def aspect_for_projection(gmtversion, *args, **kwargs):
3144 gmt = GMT(version=gmtversion, eps_mode=True)
3146 if gmt.is_gmt5():
3147 gmt.psbasemap('-B+gblack', finish=True, *args, **kwargs)
3148 fn = gmt.tempfilename('test.eps')
3149 gmt.save(fn, crop_eps_mode=True)
3150 with open(fn, 'rb') as f:
3151 s = f.read()
3153 l, b, r, t = get_bbox(s)
3154 else:
3155 gmt.psbasemap('-G0', finish=True, *args, **kwargs)
3156 l, b, r, t = gmt.bbox()
3158 return (t-b)/(r-l)
3161def text_box(
3162 text, font=0, font_size=12., angle=0, gmtversion='newest', **kwargs):
3164 gmt = GMT(version=gmtversion)
3165 if gmt.is_gmt5():
3166 row = [0, 0, text]
3167 farg = ['-F+f%gp,%s,%s+j%s' % (font_size, font, 'black', 'BL')]
3168 else:
3169 row = [0, 0, font_size, 0, font, 'BL', text]
3170 farg = []
3172 gmt.pstext(
3173 in_rows=[row],
3174 finish=True,
3175 R=(0, 1, 0, 1),
3176 J='x10p',
3177 N=True,
3178 *farg,
3179 **kwargs)
3181 fn = gmt.tempfilename() + '.ps'
3182 gmt.save(fn)
3184 (_, stderr) = subprocess.Popen(
3185 ['gs', '-q', '-dNOPAUSE', '-dBATCH', '-r720', '-sDEVICE=bbox', fn],
3186 stderr=subprocess.PIPE).communicate()
3188 dx, dy = None, None
3189 for line in stderr.splitlines():
3190 if line.startswith(b'%%HiResBoundingBox:'):
3191 l, b, r, t = [float(x) for x in line.split()[-4:]]
3192 dx, dy = r-l, t-b
3193 break
3195 return dx, dy
3198class TableLiner(object):
3199 '''
3200 Utility class to turn tables into lines.
3201 '''
3203 def __init__(self, in_columns=None, in_rows=None, encoding='utf-8'):
3204 self.in_columns = in_columns
3205 self.in_rows = in_rows
3206 self.encoding = encoding
3208 def __iter__(self):
3209 if self.in_columns is not None:
3210 for row in zip(*self.in_columns):
3211 yield (' '.join([newstr(x) for x in row])+'\n').encode(
3212 self.encoding)
3214 if self.in_rows is not None:
3215 for row in self.in_rows:
3216 yield (' '.join([newstr(x) for x in row])+'\n').encode(
3217 self.encoding)
3220class LineStreamChopper(object):
3221 '''
3222 File-like object to buffer data.
3223 '''
3225 def __init__(self, liner):
3226 self.chopsize = None
3227 self.liner = liner
3228 self.chop_iterator = None
3229 self.closed = False
3231 def _chopiter(self):
3232 buf = BytesIO()
3233 for line in self.liner:
3234 buf.write(line)
3235 buflen = buf.tell()
3236 if self.chopsize is not None and buflen >= self.chopsize:
3237 buf.seek(0)
3238 while buf.tell() <= buflen-self.chopsize:
3239 yield buf.read(self.chopsize)
3241 newbuf = BytesIO()
3242 newbuf.write(buf.read())
3243 buf.close()
3244 buf = newbuf
3246 yield(buf.getvalue())
3247 buf.close()
3249 def read(self, size=None):
3250 if self.closed:
3251 raise ValueError('Cannot read from closed LineStreamChopper.')
3252 if self.chop_iterator is None:
3253 self.chopsize = size
3254 self.chop_iterator = self._chopiter()
3256 self.chopsize = size
3257 try:
3258 return next(self.chop_iterator)
3259 except StopIteration:
3260 return ''
3262 def close(self):
3263 self.chopsize = None
3264 self.chop_iterator = None
3265 self.closed = True
3267 def flush(self):
3268 pass
3271font_tab = {
3272 0: 'Helvetica',
3273 1: 'Helvetica-Bold',
3274}
3276font_tab_rev = dict((v, k) for (k, v) in font_tab.items())
3279class GMT(object):
3280 '''
3281 A thin wrapper to GMT command execution.
3283 A dict ``config`` may be given to override some of the default GMT
3284 parameters. The ``version`` argument may be used to select a specific GMT
3285 version, which should be used with this GMT instance. The selected
3286 version of GMT has to be installed on the system, must be supported by
3287 gmtpy and gmtpy must know where to find it.
3289 Each instance of this class is used for the task of producing one PS or PDF
3290 output file.
3292 Output of a series of GMT commands is accumulated in memory and can then be
3293 saved as PS or PDF file using the :py:meth:`save` method.
3295 GMT commands are accessed as method calls to instances of this class. See
3296 the :py:meth:`__getattr__` method for details on how the method's
3297 arguments are translated into options and arguments for the GMT command.
3299 Associated with each instance of this class, a temporary directory is
3300 created, where temporary files may be created, and which is automatically
3301 deleted, when the object is destroyed. The :py:meth:`tempfilename` method
3302 may be used to get a random filename in the instance's temporary directory.
3304 Any .gmtdefaults files are ignored. The GMT class uses a fixed
3305 set of defaults, which may be altered via an argument to the constructor.
3306 If possible, GMT is run in 'isolation mode', which was introduced with GMT
3307 version 4.2.2, by setting `GMT_TMPDIR` to the instance's temporary
3308 directory. With earlier versions of GMT, problems may arise with parallel
3309 execution of more than one GMT instance.
3311 Each instance of the GMT class may pick a specific version of GMT which
3312 shall be used, so that, if multiple versions of GMT are installed on the
3313 system, different versions of GMT can be used simultaneously such that
3314 backward compatibility of the scripts can be maintained.
3316 '''
3318 def __init__(
3319 self,
3320 config=None,
3321 kontinue=None,
3322 version='newest',
3323 config_papersize=None,
3324 eps_mode=False):
3326 self.installation = get_gmt_installation(version)
3327 self.gmt_config = dict(self.installation['defaults'])
3328 self.eps_mode = eps_mode
3329 self._shutil = shutil
3331 if config:
3332 self.gmt_config.update(config)
3334 if config_papersize:
3335 if not isinstance(config_papersize, str):
3336 config_papersize = 'Custom_%ix%i' % (
3337 int(config_papersize[0]), int(config_papersize[1]))
3339 if self.is_gmt5():
3340 self.gmt_config['PS_MEDIA'] = config_papersize
3341 else:
3342 self.gmt_config['PAPER_MEDIA'] = config_papersize
3344 self.tempdir = tempfile.mkdtemp("", "gmtpy-")
3345 self.gmt_config_filename = pjoin(self.tempdir, 'gmt.conf')
3346 self.gen_gmt_config_file(self.gmt_config_filename, self.gmt_config)
3348 if kontinue is not None:
3349 self.load_unfinished(kontinue)
3350 self.needstart = False
3351 else:
3352 self.output = BytesIO()
3353 self.needstart = True
3355 self.finished = False
3357 self.environ = os.environ.copy()
3358 self.environ['GMTHOME'] = self.installation.get('home', '')
3359 # GMT isolation mode: works only properly with GMT version >= 4.2.2
3360 self.environ['GMT_TMPDIR'] = self.tempdir
3362 self.layout = None
3363 self.command_log = []
3364 self.keep_temp_dir = False
3366 def is_gmt5(self):
3367 return self.installation['version'][0] == '5'
3369 def get_version(self):
3370 return self.installation['version']
3372 def get_config(self, key):
3373 return self.gmt_config[key]
3375 def to_points(self, string):
3376 if not string:
3377 return 0
3379 unit = string[-1]
3380 if unit in _units:
3381 return float(string[:-1])/_units[unit]
3382 else:
3383 default_unit = measure_unit(self.gmt_config).lower()[0]
3384 return float(string)/_units[default_unit]
3386 def label_font_size(self):
3387 if self.is_gmt5():
3388 return self.to_points(self.gmt_config['FONT_LABEL'].split(',')[0])
3389 else:
3390 return self.to_points(self.gmt_config['LABEL_FONT_SIZE'])
3392 def label_font(self):
3393 if self.is_gmt5():
3394 return font_tab_rev(self.gmt_config['FONT_LABEL'].split(',')[1])
3395 else:
3396 return self.gmt_config['LABEL_FONT']
3398 def gen_gmt_config_file(self, config_filename, config):
3399 f = open(config_filename, 'wb')
3400 f.write(
3401 ('#\n# GMT %s Defaults file\n'
3402 % self.installation['version']).encode('ascii'))
3404 for k, v in config.items():
3405 f.write(('%s = %s\n' % (k, v)).encode('ascii'))
3406 f.close()
3408 def __del__(self):
3409 if not self.keep_temp_dir:
3410 self._shutil.rmtree(self.tempdir)
3412 def _gmtcommand(self, command, *addargs, **kwargs):
3414 '''
3415 Execute arbitrary GMT command.
3417 See docstring in __getattr__ for details.
3418 '''
3420 in_stream = kwargs.pop('in_stream', None)
3421 in_filename = kwargs.pop('in_filename', None)
3422 in_string = kwargs.pop('in_string', None)
3423 in_columns = kwargs.pop('in_columns', None)
3424 in_rows = kwargs.pop('in_rows', None)
3425 out_stream = kwargs.pop('out_stream', None)
3426 out_filename = kwargs.pop('out_filename', None)
3427 out_discard = kwargs.pop('out_discard', None)
3428 finish = kwargs.pop('finish', False)
3429 suppressdefaults = kwargs.pop('suppress_defaults', False)
3430 config_override = kwargs.pop('config', None)
3432 assert(not self.finished)
3434 # check for mutual exclusiveness on input and output possibilities
3435 assert(1 >= len(
3436 [x for x in [
3437 in_stream, in_filename, in_string, in_columns, in_rows]
3438 if x is not None]))
3439 assert(1 >= len([x for x in [out_stream, out_filename, out_discard]
3440 if x is not None]))
3442 options = []
3444 gmt_config = self.gmt_config
3445 if not self.is_gmt5():
3446 gmt_config_filename = self.gmt_config_filename
3447 if config_override:
3448 gmt_config = self.gmt_config.copy()
3449 gmt_config.update(config_override)
3450 gmt_config_override_filename = pjoin(
3451 self.tempdir, 'gmtdefaults_override')
3452 self.gen_gmt_config_file(
3453 gmt_config_override_filename, gmt_config)
3454 gmt_config_filename = gmt_config_override_filename
3456 else: # gmt5 needs override variables as --VAR=value
3457 if config_override:
3458 for k, v in config_override.items():
3459 options.append('--%s=%s' % (k, v))
3461 if out_discard:
3462 out_filename = '/dev/null'
3464 out_mustclose = False
3465 if out_filename is not None:
3466 out_mustclose = True
3467 out_stream = open(out_filename, 'wb')
3469 if in_filename is not None:
3470 in_stream = open(in_filename, 'rb')
3472 if in_string is not None:
3473 in_stream = BytesIO(in_string)
3475 encoding_gmt = gmt_config.get(
3476 'PS_CHAR_ENCODING',
3477 gmt_config.get('CHAR_ENCODING', 'ISOLatin1+'))
3479 encoding = encoding_gmt_to_python[encoding_gmt.lower()]
3481 if in_columns is not None or in_rows is not None:
3482 in_stream = LineStreamChopper(TableLiner(in_columns=in_columns,
3483 in_rows=in_rows,
3484 encoding=encoding))
3486 # convert option arguments to strings
3487 for k, v in kwargs.items():
3488 if len(k) > 1:
3489 raise GmtPyError('Found illegal keyword argument "%s" '
3490 'while preparing options for command "%s"'
3491 % (k, command))
3493 if type(v) is bool:
3494 if v:
3495 options.append('-%s' % k)
3496 elif type(v) is tuple or type(v) is list:
3497 options.append('-%s' % k + '/'.join([str(x) for x in v]))
3498 else:
3499 options.append('-%s%s' % (k, str(v)))
3501 # if not redirecting to an external sink, handle -K -O
3502 if out_stream is None:
3503 if not finish:
3504 options.append('-K')
3505 else:
3506 self.finished = True
3508 if not self.needstart:
3509 options.append('-O')
3510 else:
3511 self.needstart = False
3513 out_stream = self.output
3515 # run the command
3516 if self.is_gmt5():
3517 args = [pjoin(self.installation['bin'], 'gmt'), command]
3518 else:
3519 args = [pjoin(self.installation['bin'], command)]
3521 if not os.path.isfile(args[0]):
3522 raise OSError('No such file: %s' % args[0])
3523 args.extend(options)
3524 args.extend(addargs)
3525 if not self.is_gmt5() and not suppressdefaults:
3526 # does not seem to work with GMT 5 (and should not be necessary
3527 args.append('+'+gmt_config_filename)
3529 bs = 2048
3530 p = subprocess.Popen(args, stdin=subprocess.PIPE,
3531 stdout=subprocess.PIPE, bufsize=bs,
3532 env=self.environ)
3533 while True:
3534 cr, cw, cx = select([p.stdout], [p.stdin], [])
3535 if cr:
3536 out_stream.write(p.stdout.read(bs))
3537 if cw:
3538 if in_stream is not None:
3539 data = in_stream.read(bs)
3540 if len(data) == 0:
3541 break
3542 p.stdin.write(data)
3543 else:
3544 break
3545 if not cr and not cw:
3546 break
3548 p.stdin.close()
3550 while True:
3551 data = p.stdout.read(bs)
3552 if len(data) == 0:
3553 break
3554 out_stream.write(data)
3556 p.stdout.close()
3558 retcode = p.wait()
3560 if in_stream is not None:
3561 in_stream.close()
3563 if out_mustclose:
3564 out_stream.close()
3566 if retcode != 0:
3567 self.keep_temp_dir = True
3568 raise GMTError('Command %s returned an error. '
3569 'While executing command:\n%s'
3570 % (command, escape_shell_args(args)))
3572 self.command_log.append(args)
3574 def __getattr__(self, command):
3576 '''
3577 Maps to call self._gmtcommand(command, \\*addargs, \\*\\*kwargs).
3579 Execute arbitrary GMT command.
3581 Run a GMT command and by default append its postscript output to the
3582 output file maintained by the GMT instance on which this method is
3583 called.
3585 Except for a few keyword arguments listed below, any ``kwargs`` and
3586 ``addargs`` are converted into command line options and arguments and
3587 passed to the GMT command. Numbers in keyword arguments are converted
3588 into strings. E.g. ``S=10`` is translated into ``'-S10'``. Tuples of
3589 numbers or strings are converted into strings where the elements of the
3590 tuples are separated by slashes '/'. E.g. ``R=(10, 10, 20, 20)`` is
3591 translated into ``'-R10/10/20/20'``. Options with a boolean argument
3592 are only appended to the GMT command, if their values are True.
3594 If no output redirection is in effect, the -K and -O options are
3595 handled by gmtpy and thus should not be specified. Use
3596 ``out_discard=True`` if you don't want -K or -O beeing added, but are
3597 not interested in the output.
3599 The standard input of the GMT process is fed by data selected with one
3600 of the following ``in_*`` keyword arguments:
3602 =============== =======================================================
3603 ``in_stream`` Data is read from an open file like object.
3604 ``in_filename`` Data is read from the given file.
3605 ``in_string`` String content is dumped to the process.
3606 ``in_columns`` A 2D nested iterable whose elements can be accessed as
3607 ``in_columns[icolumn][irow]`` is converted into an
3608 ascii
3609 table, which is fed to the process.
3610 ``in_rows`` A 2D nested iterable whos elements can be accessed as
3611 ``in_rows[irow][icolumn]`` is converted into an ascii
3612 table, which is fed to the process.
3613 =============== =======================================================
3615 The standard output of the GMT process may be redirected by one of the
3616 following options:
3618 ================= =====================================================
3619 ``out_stream`` Output is fed to an open file like object.
3620 ``out_filename`` Output is dumped to the given file.
3621 ``out_discard`` If True, output is dumped to :file:`/dev/null`.
3622 ================= =====================================================
3624 Additional keyword arguments:
3626 ===================== =================================================
3627 ``config`` Dict with GMT defaults which override the
3628 currently active set of defaults exclusively
3629 during this call.
3630 ``finish`` If True, the postscript file, which is maintained
3631 by the GMT instance is finished, and no further
3632 plotting is allowed.
3633 ``suppress_defaults`` Suppress appending of the ``'+gmtdefaults'``
3634 option to the command.
3635 ===================== =================================================
3637 '''
3639 def f(*args, **kwargs):
3640 return self._gmtcommand(command, *args, **kwargs)
3641 return f
3643 def tempfilename(self, name=None):
3644 '''
3645 Get filename for temporary file in the private temp directory.
3647 If no ``name`` argument is given, a random name is picked. If
3648 ``name`` is given, returns a path ending in that ``name``.
3649 '''
3651 if not name:
3652 name = ''.join(
3653 [random.choice('abcdefghijklmnopqrstuvwxyz')
3654 for i in range(10)])
3656 fn = pjoin(self.tempdir, name)
3657 return fn
3659 def tempfile(self, name=None):
3660 '''
3661 Create and open a file in the private temp directory.
3662 '''
3664 fn = self.tempfilename(name)
3665 f = open(fn, 'wb')
3666 return f, fn
3668 def save_unfinished(self, filename):
3669 out = open(filename, 'wb')
3670 out.write(self.output.getvalue())
3671 out.close()
3673 def load_unfinished(self, filename):
3674 self.output = BytesIO()
3675 self.finished = False
3676 inp = open(filename, 'rb')
3677 self.output.write(inp.read())
3678 inp.close()
3680 def dump(self, ident):
3681 filename = self.tempfilename('breakpoint-%s' % ident)
3682 self.save_unfinished(filename)
3684 def load(self, ident):
3685 filename = self.tempfilename('breakpoint-%s' % ident)
3686 self.load_unfinished(filename)
3688 def save(self, filename=None, bbox=None, resolution=150, oversample=2.,
3689 width=None, height=None, size=None, crop_eps_mode=False,
3690 psconvert=False):
3692 '''
3693 Finish and save figure as PDF, PS or PPM file.
3695 If filename ends with ``'.pdf'`` a PDF file is created by piping the
3696 GMT output through :program:`gmtpy-epstopdf`.
3698 If filename ends with ``'.png'`` a PNG file is created by running
3699 :program:`gmtpy-epstopdf`, :program:`pdftocairo` and
3700 :program:`convert`. ``resolution`` specifies the resolution in DPI for
3701 raster file formats. Rasterization is done at a higher resolution if
3702 ``oversample`` is set to a value higher than one. The output image size
3703 can also be controlled by setting ``width``, ``height`` or ``size``
3704 instead of ``resolution``. When ``size`` is given, the image is scaled
3705 so that ``max(width, height) == size``.
3707 The bounding box is set according to the values given in ``bbox``.
3708 '''
3710 if not self.finished:
3711 self.psxy(R=True, J=True, finish=True)
3713 if filename:
3714 tempfn = pjoin(self.tempdir, 'incomplete')
3715 out = open(tempfn, 'wb')
3716 else:
3717 out = sys.stdout
3719 if bbox and not self.is_gmt5():
3720 out.write(replace_bbox(bbox, self.output.getvalue()))
3721 else:
3722 out.write(self.output.getvalue())
3724 if filename:
3725 out.close()
3727 if filename.endswith('.ps') or (
3728 not self.is_gmt5() and filename.endswith('.eps')):
3730 shutil.move(tempfn, filename)
3731 return
3733 if self.is_gmt5():
3734 if crop_eps_mode:
3735 addarg = ['-A']
3736 else:
3737 addarg = []
3739 subprocess.call(
3740 [pjoin(self.installation['bin'], 'gmt'), 'psconvert',
3741 '-Te', '-F%s' % tempfn + '.eps', tempfn, ] + addarg)
3743 if bbox:
3744 with open(tempfn + '.eps', 'rb') as fin:
3745 with open(tempfn + '-fixbb.eps', 'wb') as fout:
3746 replace_bbox(bbox, fin, fout)
3748 shutil.move(tempfn + '-fixbb.eps', tempfn + '.eps')
3750 else:
3751 shutil.move(tempfn, tempfn + '.eps')
3753 if filename.endswith('.eps'):
3754 shutil.move(tempfn + '.eps', filename)
3755 return
3757 elif filename.endswith('.pdf'):
3758 if psconvert:
3759 gmt_bin = pjoin(self.installation['bin'], 'gmt')
3760 subprocess.call([gmt_bin, 'psconvert', tempfn + '.eps', '-Tf',
3761 '-F' + filename])
3762 else:
3763 subprocess.call(['gmtpy-epstopdf', '--res=%i' % resolution,
3764 '--outfile=' + filename, tempfn + '.eps'])
3765 else:
3766 subprocess.call([
3767 'gmtpy-epstopdf',
3768 '--res=%i' % (resolution * oversample),
3769 '--outfile=' + tempfn + '.pdf', tempfn + '.eps'])
3771 convert_graph(
3772 tempfn + '.pdf', filename,
3773 resolution=resolution, oversample=oversample,
3774 size=size, width=width, height=height)
3776 def bbox(self):
3777 return get_bbox(self.output.getvalue())
3779 def get_command_log(self):
3780 '''
3781 Get the command log.
3782 '''
3784 return self.command_log
3786 def __str__(self):
3787 s = ''
3788 for com in self.command_log:
3789 s += com[0] + "\n " + "\n ".join(com[1:]) + "\n\n"
3790 return s
3792 def page_size_points(self):
3793 '''
3794 Try to get paper size of output postscript file in points.
3795 '''
3797 pm = paper_media(self.gmt_config).lower()
3798 if pm.endswith('+') or pm.endswith('-'):
3799 pm = pm[:-1]
3801 orient = page_orientation(self.gmt_config).lower()
3803 if pm in all_paper_sizes():
3805 if orient == 'portrait':
3806 return get_paper_size(pm)
3807 else:
3808 return get_paper_size(pm)[1], get_paper_size(pm)[0]
3810 m = re.match(r'custom_([0-9.]+)([cimp]?)x([0-9.]+)([cimp]?)', pm)
3811 if m:
3812 w, uw, h, uh = m.groups()
3813 w, h = float(w), float(h)
3814 if uw:
3815 w *= _units[uw]
3816 if uh:
3817 h *= _units[uh]
3818 if orient == 'portrait':
3819 return w, h
3820 else:
3821 return h, w
3823 return None, None
3825 def default_layout(self, with_palette=False):
3826 '''
3827 Get a default layout for the output page.
3829 One of three different layouts is choosen, depending on the
3830 `PAPER_MEDIA` setting in the GMT configuration dict.
3832 If `PAPER_MEDIA` ends with a ``'+'`` (EPS output is selected), a
3833 :py:class:`FrameLayout` is centered on the page, whose size is
3834 controlled by its center widget's size plus the margins of the
3835 :py:class:`FrameLayout`.
3837 If `PAPER_MEDIA` indicates, that a custom page size is wanted by
3838 starting with ``'Custom_'``, a :py:class:`FrameLayout` is used to fill
3839 the complete page. The center widget's size is then controlled by the
3840 page's size minus the margins of the :py:class:`FrameLayout`.
3842 In any other case, two FrameLayouts are nested, such that the outer
3843 layout attaches a 1 cm (printer) margin around the complete page, and
3844 the inner FrameLayout's center widget takes up as much space as
3845 possible under the constraint, that an aspect ratio of 1/golden_ratio
3846 is preserved.
3848 In any case, a reference to the innermost :py:class:`FrameLayout`
3849 instance is returned. The top-level layout can be accessed by calling
3850 :py:meth:`Widget.get_parent` on the returned layout.
3851 '''
3853 if self.layout is None:
3854 w, h = self.page_size_points()
3856 if w is None or h is None:
3857 raise GmtPyError("Can't determine page size for layout")
3859 pm = paper_media(self.gmt_config).lower()
3861 if with_palette:
3862 palette_layout = GridLayout(3, 1)
3863 spacer = palette_layout.get_widget(1, 0)
3864 palette_widget = palette_layout.get_widget(2, 0)
3865 spacer.set_horizontal(0.5*cm)
3866 palette_widget.set_horizontal(0.5*cm)
3868 if pm.endswith('+') or self.eps_mode:
3869 outer = CenterLayout()
3870 outer.set_policy((w, h), (0., 0.))
3871 inner = FrameLayout()
3872 outer.set_widget(inner)
3873 if with_palette:
3874 inner.set_widget('center', palette_layout)
3875 widget = palette_layout
3876 else:
3877 widget = inner.get_widget('center')
3878 widget.set_policy((w/golden_ratio, 0.), (0., 0.),
3879 aspect=1./golden_ratio)
3880 mw = 3.0*cm
3881 inner.set_fixed_margins(
3882 mw, mw, mw/golden_ratio, mw/golden_ratio)
3883 self.layout = inner
3885 elif pm.startswith('custom_'):
3886 layout = FrameLayout()
3887 layout.set_policy((w, h), (0., 0.))
3888 mw = 3.0*cm
3889 layout.set_min_margins(
3890 mw, mw, mw/golden_ratio, mw/golden_ratio)
3891 if with_palette:
3892 layout.set_widget('center', palette_layout)
3893 self.layout = layout
3894 else:
3895 outer = FrameLayout()
3896 outer.set_policy((w, h), (0., 0.))
3897 outer.set_fixed_margins(1.*cm, 1.*cm, 1.*cm, 1.*cm)
3899 inner = FrameLayout()
3900 outer.set_widget('center', inner)
3901 mw = 3.0*cm
3902 inner.set_min_margins(mw, mw, mw/golden_ratio, mw/golden_ratio)
3903 if with_palette:
3904 inner.set_widget('center', palette_layout)
3905 widget = palette_layout
3906 else:
3907 widget = inner.get_widget('center')
3909 widget.set_aspect(1./golden_ratio)
3911 self.layout = inner
3913 return self.layout
3915 def draw_layout(self, layout):
3916 '''
3917 Use psxy to draw layout; for debugging
3918 '''
3920 # corners = layout.get_corners(descend=True)
3921 rects = num.array(layout.get_sizes(), dtype=float)
3922 rects_wid = rects[:, 0, 0]
3923 rects_hei = rects[:, 0, 1]
3924 rects_center_x = rects[:, 1, 0] + rects_wid*0.5
3925 rects_center_y = rects[:, 1, 1] + rects_hei*0.5
3926 nrects = len(rects)
3927 prects = (rects_center_x, rects_center_y, num.arange(nrects),
3928 num.zeros(nrects), rects_hei, rects_wid)
3930 # points = num.array(corners, dtype=float)
3932 cptfile = self.tempfilename() + '.cpt'
3933 self.makecpt(
3934 C='ocean',
3935 T='%g/%g/%g' % (-nrects, nrects, 1),
3936 Z=True,
3937 out_filename=cptfile, suppress_defaults=True)
3939 bb = layout.bbox()
3940 self.psxy(
3941 in_columns=prects,
3942 C=cptfile,
3943 W='1p',
3944 S='J',
3945 R=(bb[0], bb[2], bb[1], bb[3]),
3946 *layout.XYJ())
3949def simpleconf_to_ax(conf, axname):
3950 c = {}
3951 x = axname
3952 for x in ('', axname):
3953 for k in ('label', 'unit', 'scaled_unit', 'scaled_unit_factor',
3954 'space', 'mode', 'approx_ticks', 'limits', 'masking', 'inc',
3955 'snap'):
3957 if x+k in conf:
3958 c[k] = conf[x+k]
3960 return Ax(**c)
3963class DensityPlotDef(object):
3964 def __init__(self, data, cpt='ocean', tension=0.7, size=(640, 480),
3965 contour=False, method='surface', zscaler=None, **extra):
3966 self.data = data
3967 self.cpt = cpt
3968 self.tension = tension
3969 self.size = size
3970 self.contour = contour
3971 self.method = method
3972 self.zscaler = zscaler
3973 self.extra = extra
3976class TextDef(object):
3977 def __init__(
3978 self,
3979 data,
3980 size=9,
3981 justify='MC',
3982 fontno=0,
3983 offset=(0, 0),
3984 color='black'):
3986 self.data = data
3987 self.size = size
3988 self.justify = justify
3989 self.fontno = fontno
3990 self.offset = offset
3991 self.color = color
3994class Simple(object):
3995 def __init__(self, gmtconfig=None, gmtversion='newest', **simple_config):
3996 self.data = []
3997 self.symbols = []
3998 self.config = copy.deepcopy(simple_config)
3999 self.gmtconfig = gmtconfig
4000 self.density_plot_defs = []
4001 self.text_defs = []
4003 self.gmtversion = gmtversion
4005 self.data_x = []
4006 self.symbols_x = []
4008 self.data_y = []
4009 self.symbols_y = []
4011 self.default_config = {}
4012 self.set_defaults(width=15.*cm,
4013 height=15.*cm / golden_ratio,
4014 margins=(2.*cm, 2.*cm, 2.*cm, 2.*cm),
4015 with_palette=False,
4016 palette_offset=0.5*cm,
4017 palette_width=None,
4018 palette_height=None,
4019 zlabeloffset=2*cm,
4020 draw_layout=False)
4022 self.setup_defaults()
4023 self.fixate_widget_aspect = False
4025 def setup_defaults(self):
4026 pass
4028 def set_defaults(self, **kwargs):
4029 self.default_config.update(kwargs)
4031 def plot(self, data, symbol=''):
4032 self.data.append(data)
4033 self.symbols.append(symbol)
4035 def density_plot(self, data, **kwargs):
4036 dpd = DensityPlotDef(data, **kwargs)
4037 self.density_plot_defs.append(dpd)
4039 def text(self, data, **kwargs):
4040 dpd = TextDef(data, **kwargs)
4041 self.text_defs.append(dpd)
4043 def plot_x(self, data, symbol=''):
4044 self.data_x.append(data)
4045 self.symbols_x.append(symbol)
4047 def plot_y(self, data, symbol=''):
4048 self.data_y.append(data)
4049 self.symbols_y.append(symbol)
4051 def set(self, **kwargs):
4052 self.config.update(kwargs)
4054 def setup_base(self, conf):
4055 w = conf.pop('width')
4056 h = conf.pop('height')
4057 margins = conf.pop('margins')
4059 gmtconfig = {}
4060 if self.gmtconfig is not None:
4061 gmtconfig.update(self.gmtconfig)
4063 gmt = GMT(
4064 version=self.gmtversion,
4065 config=gmtconfig,
4066 config_papersize='Custom_%ix%i' % (w, h))
4068 layout = gmt.default_layout(with_palette=conf['with_palette'])
4069 layout.set_min_margins(*margins)
4070 if conf['with_palette']:
4071 widget = layout.get_widget().get_widget(0, 0)
4072 spacer = layout.get_widget().get_widget(1, 0)
4073 spacer.set_horizontal(conf['palette_offset'])
4074 palette_widget = layout.get_widget().get_widget(2, 0)
4075 if conf['palette_width'] is not None:
4076 palette_widget.set_horizontal(conf['palette_width'])
4077 if conf['palette_height'] is not None:
4078 palette_widget.set_vertical(conf['palette_height'])
4079 widget.set_vertical(h-margins[2]-margins[3]-0.03*cm)
4080 return gmt, layout, widget, palette_widget
4081 else:
4082 widget = layout.get_widget()
4083 return gmt, layout, widget, None
4085 def setup_projection(self, widget, scaler, conf):
4086 pass
4088 def setup_scaling(self, conf):
4089 ndims = 2
4090 if self.density_plot_defs:
4091 ndims = 3
4093 axes = [simpleconf_to_ax(conf, x) for x in 'xyz'[:ndims]]
4095 data_all = []
4096 data_all.extend(self.data)
4097 for dsd in self.density_plot_defs:
4098 if dsd.zscaler is None:
4099 data_all.append(dsd.data)
4100 else:
4101 data_all.append(dsd.data[:2])
4102 data_chopped = [ds[:ndims] for ds in data_all]
4104 scaler = ScaleGuru(data_chopped, axes=axes[:ndims])
4106 self.setup_scaling_plus(scaler, axes[:ndims])
4108 return scaler
4110 def setup_scaling_plus(self, scaler, axes):
4111 pass
4113 def setup_scaling_extra(self, scaler, conf):
4115 scaler_x = scaler.copy()
4116 scaler_x.data_ranges[1] = (0., 1.)
4117 scaler_x.axes[1].mode = 'off'
4119 scaler_y = scaler.copy()
4120 scaler_y.data_ranges[0] = (0., 1.)
4121 scaler_y.axes[0].mode = 'off'
4123 return scaler_x, scaler_y
4125 def draw_density(self, gmt, widget, scaler):
4127 R = scaler.R()
4128 # par = scaler.get_params()
4129 rxyj = R + widget.XYJ()
4130 innerticks = False
4131 for dpd in self.density_plot_defs:
4133 fn_cpt = gmt.tempfilename() + '.cpt'
4135 if dpd.zscaler is not None:
4136 s = dpd.zscaler
4137 else:
4138 s = scaler
4140 gmt.makecpt(C=dpd.cpt, out_filename=fn_cpt, *s.T())
4142 fn_grid = gmt.tempfilename()
4144 fn_mean = gmt.tempfilename()
4146 if dpd.method in ('surface', 'triangulate'):
4147 gmt.blockmean(in_columns=dpd.data,
4148 I='%i+/%i+' % dpd.size, # noqa
4149 out_filename=fn_mean, *R)
4151 if dpd.method == 'surface':
4152 gmt.surface(
4153 in_filename=fn_mean,
4154 T=dpd.tension,
4155 G=fn_grid,
4156 I='%i+/%i+' % dpd.size, # noqa
4157 out_discard=True,
4158 *R)
4160 if dpd.method == 'triangulate':
4161 gmt.triangulate(
4162 in_filename=fn_mean,
4163 G=fn_grid,
4164 I='%i+/%i+' % dpd.size, # noqa
4165 out_discard=True,
4166 V=True,
4167 *R)
4169 if gmt.is_gmt5():
4170 gmt.grdimage(fn_grid, C=fn_cpt, E='i', n='l', *rxyj)
4172 else:
4173 gmt.grdimage(fn_grid, C=fn_cpt, E='i', S='l', *rxyj)
4175 if dpd.contour:
4176 gmt.grdcontour(fn_grid, C=fn_cpt, W='0.5p,black', *rxyj)
4177 innerticks = '0.5p,black'
4179 os.remove(fn_grid)
4180 os.remove(fn_mean)
4182 if dpd.method == 'fillcontour':
4183 extra = dict(C=fn_cpt)
4184 extra.update(dpd.extra)
4185 gmt.pscontour(in_columns=dpd.data,
4186 I=True, *rxyj, **extra) # noqa
4188 if dpd.method == 'contour':
4189 extra = dict(W='0.5p,black', C=fn_cpt)
4190 extra.update(dpd.extra)
4191 gmt.pscontour(in_columns=dpd.data, *rxyj, **extra)
4193 return fn_cpt, innerticks
4195 def draw_basemap(self, gmt, widget, scaler):
4196 gmt.psbasemap(*(widget.JXY() + scaler.RB(ax_projection=True)))
4198 def draw(self, gmt, widget, scaler):
4199 rxyj = scaler.R() + widget.JXY()
4200 for dat, sym in zip(self.data, self.symbols):
4201 gmt.psxy(in_columns=dat, *(sym.split()+rxyj))
4203 def post_draw(self, gmt, widget, scaler):
4204 pass
4206 def pre_draw(self, gmt, widget, scaler):
4207 pass
4209 def draw_extra(self, gmt, widget, scaler_x, scaler_y):
4211 for dat, sym in zip(self.data_x, self.symbols_x):
4212 gmt.psxy(in_columns=dat,
4213 *(sym.split() + scaler_x.R() + widget.JXY()))
4215 for dat, sym in zip(self.data_y, self.symbols_y):
4216 gmt.psxy(in_columns=dat,
4217 *(sym.split() + scaler_y.R() + widget.JXY()))
4219 def draw_text(self, gmt, widget, scaler):
4221 rxyj = scaler.R() + widget.JXY()
4222 for td in self.text_defs:
4223 x, y = td.data[0:2]
4224 text = td.data[-1]
4225 size = td.size
4226 angle = 0
4227 fontno = td.fontno
4228 justify = td.justify
4229 color = td.color
4230 if gmt.is_gmt5():
4231 gmt.pstext(
4232 in_rows=[(x, y, text)],
4233 F='+f%gp,%s,%s+a%g+j%s' % (
4234 size, fontno, color, angle, justify),
4235 D='%gp/%gp' % td.offset, *rxyj)
4236 else:
4237 gmt.pstext(
4238 in_rows=[(x, y, size, angle, fontno, justify, text)],
4239 D='%gp/%gp' % td.offset, *rxyj)
4241 def save(self, filename, resolution=150):
4243 conf = dict(self.default_config)
4244 conf.update(self.config)
4246 gmt, layout, widget, palette_widget = self.setup_base(conf)
4247 scaler = self.setup_scaling(conf)
4248 scaler_x, scaler_y = self.setup_scaling_extra(scaler, conf)
4250 self.setup_projection(widget, scaler, conf)
4251 if self.fixate_widget_aspect:
4252 aspect = aspect_for_projection(
4253 gmt.installation['version'], *(widget.J() + scaler.R()))
4255 widget.set_aspect(aspect)
4257 if conf['draw_layout']:
4258 gmt.draw_layout(layout)
4259 cptfile = None
4260 if self.density_plot_defs:
4261 cptfile, innerticks = self.draw_density(gmt, widget, scaler)
4262 self.pre_draw(gmt, widget, scaler)
4263 self.draw(gmt, widget, scaler)
4264 self.post_draw(gmt, widget, scaler)
4265 self.draw_extra(gmt, widget, scaler_x, scaler_y)
4266 self.draw_text(gmt, widget, scaler)
4267 self.draw_basemap(gmt, widget, scaler)
4269 if palette_widget and cptfile:
4270 nice_palette(gmt, palette_widget, scaler, cptfile,
4271 innerticks=innerticks,
4272 zlabeloffset=conf['zlabeloffset'])
4274 gmt.save(filename, resolution=resolution)
4277class LinLinPlot(Simple):
4278 pass
4281class LogLinPlot(Simple):
4283 def setup_defaults(self):
4284 self.set_defaults(xmode='min-max')
4286 def setup_projection(self, widget, scaler, conf):
4287 widget['J'] = '-JX%(width)gpl/%(height)gp'
4288 scaler['B'] = '-B2:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen'
4291class LinLogPlot(Simple):
4293 def setup_defaults(self):
4294 self.set_defaults(ymode='min-max')
4296 def setup_projection(self, widget, scaler, conf):
4297 widget['J'] = '-JX%(width)gp/%(height)gpl'
4298 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/2:%(ylabel)s:WSen'
4301class LogLogPlot(Simple):
4303 def setup_defaults(self):
4304 self.set_defaults(mode='min-max')
4306 def setup_projection(self, widget, scaler, conf):
4307 widget['J'] = '-JX%(width)gpl/%(height)gpl'
4308 scaler['B'] = '-B2:%(xlabel)s:/2:%(ylabel)s:WSen'
4311class AziDistPlot(Simple):
4313 def __init__(self, *args, **kwargs):
4314 Simple.__init__(self, *args, **kwargs)
4315 self.fixate_widget_aspect = True
4317 def setup_defaults(self):
4318 self.set_defaults(
4319 height=15.*cm,
4320 width=15.*cm,
4321 xmode='off',
4322 xlimits=(0., 360.),
4323 xinc=45.)
4325 def setup_projection(self, widget, scaler, conf):
4326 widget['J'] = '-JPa%(width)gp'
4328 def setup_scaling_plus(self, scaler, axes):
4329 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/%(yinc)g:%(ylabel)s:N'
4332class MPlot(Simple):
4334 def __init__(self, *args, **kwargs):
4335 Simple.__init__(self, *args, **kwargs)
4336 self.fixate_widget_aspect = True
4338 def setup_defaults(self):
4339 self.set_defaults(xmode='min-max', ymode='min-max')
4341 def setup_projection(self, widget, scaler, conf):
4342 par = scaler.get_params()
4343 lon0 = (par['xmin'] + par['xmax'])/2.
4344 lat0 = (par['ymin'] + par['ymax'])/2.
4345 sll = '%g/%g' % (lon0, lat0)
4346 widget['J'] = '-JM' + sll + '/%(width)gp'
4347 scaler['B'] = \
4348 '-B%(xinc)gg%(xinc)g:%(xlabel)s:/%(yinc)gg%(yinc)g:%(ylabel)s:WSen'
4351def nice_palette(gmt, widget, scaleguru, cptfile, zlabeloffset=0.8*inch,
4352 innerticks=True):
4354 par = scaleguru.get_params()
4355 par_ax = scaleguru.get_params(ax_projection=True)
4356 nz_palette = int(widget.height()/inch * 300)
4357 px = num.zeros(nz_palette*2)
4358 px[1::2] += 1
4359 pz = num.linspace(par['zmin'], par['zmax'], nz_palette).repeat(2)
4360 pdz = pz[2]-pz[0]
4361 palgrdfile = gmt.tempfilename()
4362 pal_r = (0, 1, par['zmin'], par['zmax'])
4363 pal_ax_r = (0, 1, par_ax['zmin'], par_ax['zmax'])
4364 gmt.xyz2grd(
4365 G=palgrdfile, R=pal_r,
4366 I=(1, pdz), in_columns=(px, pz, pz), # noqa
4367 out_discard=True)
4369 gmt.grdimage(palgrdfile, R=pal_r, C=cptfile, *widget.JXY())
4370 if isinstance(innerticks, str):
4371 tickpen = innerticks
4372 gmt.grdcontour(palgrdfile, W=tickpen, R=pal_r, C=cptfile,
4373 *widget.JXY())
4375 negpalwid = '%gp' % -widget.width()
4376 if not isinstance(innerticks, str) and innerticks:
4377 ticklen = negpalwid
4378 else:
4379 ticklen = '0p'
4381 TICK_LENGTH_PARAM = 'MAP_TICK_LENGTH' if gmt.is_gmt5() else 'TICK_LENGTH'
4382 gmt.psbasemap(
4383 R=pal_ax_r, B='4::/%(zinc)g::nsw' % par_ax,
4384 config={TICK_LENGTH_PARAM: ticklen},
4385 *widget.JXY())
4387 if innerticks:
4388 gmt.psbasemap(
4389 R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax,
4390 config={TICK_LENGTH_PARAM: '0p'},
4391 *widget.JXY())
4392 else:
4393 gmt.psbasemap(R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax, *widget.JXY())
4395 if par_ax['zlabel']:
4396 label_font = gmt.label_font()
4397 label_font_size = gmt.label_font_size()
4398 label_offset = zlabeloffset
4399 gmt.pstext(
4400 R=(0, 1, 0, 2), D="%gp/0p" % label_offset,
4401 N=True,
4402 in_rows=[(1, 1, label_font_size, -90, label_font, 'CB',
4403 par_ax['zlabel'])],
4404 *widget.JXY())