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 x = to_array(nc.variables[kx])
1541 y = to_array(nc.variables[ky])
1542 z = to_array(nc.variables['z'])
1544 nc.close()
1545 return x, y, z
1548def centers_to_edges(asorted):
1549 return (asorted[1:] + asorted[:-1])/2.
1552def nvals(asorted):
1553 eps = (asorted[-1]-asorted[0])/asorted.size
1554 return num.sum(asorted[1:] - asorted[:-1] >= eps) + 1
1557def guess_vals(asorted):
1558 eps = (asorted[-1]-asorted[0])/asorted.size
1559 indis = num.nonzero(asorted[1:] - asorted[:-1] >= eps)[0]
1560 indis = num.concatenate((num.array([0]), indis+1,
1561 num.array([asorted.size])))
1562 asum = num.zeros(asorted.size+1)
1563 asum[1:] = num.cumsum(asorted)
1564 return (asum[indis[1:]] - asum[indis[:-1]]) / (indis[1:]-indis[:-1])
1567def blockmean(asorted, b):
1568 indis = num.nonzero(asorted[1:] - asorted[:-1])[0]
1569 indis = num.concatenate((num.array([0]), indis+1,
1570 num.array([asorted.size])))
1571 bsum = num.zeros(b.size+1)
1572 bsum[1:] = num.cumsum(b)
1573 return (
1574 asorted[indis[:-1]],
1575 (bsum[indis[1:]] - bsum[indis[:-1]]) / (indis[1:]-indis[:-1]))
1578def griddata_regular(x, y, z, xvals, yvals):
1579 nx, ny = xvals.size, yvals.size
1580 xindi = num.digitize(x, centers_to_edges(xvals))
1581 yindi = num.digitize(y, centers_to_edges(yvals))
1583 zindi = yindi*nx+xindi
1584 order = num.argsort(zindi)
1585 z = z[order]
1586 zindi = zindi[order]
1588 zindi, z = blockmean(zindi, z)
1589 znew = num.empty(nx*ny, dtype=float)
1590 znew[:] = num.nan
1591 znew[zindi] = z
1592 return znew.reshape(ny, nx)
1595def guess_field_size(x_sorted, y_sorted, z=None, mode=None):
1596 critical_fraction = 1./num.e - 0.014*3
1597 xs = x_sorted
1598 ys = y_sorted
1599 nxs, nys = nvals(xs), nvals(ys)
1600 if mode == 'nonrandom':
1601 return nxs, nys, 0
1602 elif xs.size == nxs*nys:
1603 # exact match
1604 return nxs, nys, 0
1605 elif nxs >= xs.size*critical_fraction and nys >= xs.size*critical_fraction:
1606 # possibly randomly sampled
1607 nxs = int(math.sqrt(xs.size))
1608 nys = nxs
1609 return nxs, nys, 2
1610 else:
1611 return nxs, nys, 1
1614def griddata_auto(x, y, z, mode=None):
1615 '''
1616 Grid tabular XYZ data by binning.
1618 This function does some extra work to guess the size of the grid. This
1619 should work fine if the input values are already defined on an rectilinear
1620 grid, even if data points are missing or duplicated. This routine also
1621 tries to detect a random distribution of input data and in that case
1622 creates a grid of size sqrt(N) x sqrt(N).
1624 The points do not have to be given in any particular order. Grid nodes
1625 without data are assigned the NaN value. If multiple data points map to the
1626 same grid node, their average is assigned to the grid node.
1627 '''
1629 x, y, z = [num.asarray(X) for X in (x, y, z)]
1630 assert x.size == y.size == z.size
1631 xs, ys = num.sort(x), num.sort(y)
1632 nx, ny, badness = guess_field_size(xs, ys, z, mode=mode)
1633 if badness <= 1:
1634 xf = guess_vals(xs)
1635 yf = guess_vals(ys)
1636 zf = griddata_regular(x, y, z, xf, yf)
1637 else:
1638 xf = num.linspace(xs[0], xs[-1], nx)
1639 yf = num.linspace(ys[0], ys[-1], ny)
1640 zf = griddata_regular(x, y, z, xf, yf)
1642 return xf, yf, zf
1645def tabledata(xf, yf, zf):
1646 assert yf.size, xf.size == zf.shape
1647 x = num.tile(xf, yf.size)
1648 y = num.repeat(yf, xf.size)
1649 z = zf.flatten()
1650 return x, y, z
1653def double1d(a):
1654 a2 = num.empty(a.size*2-1)
1655 a2[::2] = a
1656 a2[1::2] = (a[:-1] + a[1:])/2.
1657 return a2
1660def double2d(f):
1661 f2 = num.empty((f.shape[0]*2-1, f.shape[1]*2-1))
1662 f2[:, :] = num.nan
1663 f2[::2, ::2] = f
1664 f2[1::2, ::2] = (f[:-1, :] + f[1:, :])/2.
1665 f2[::2, 1::2] = (f[:, :-1] + f[:, 1:])/2.
1666 f2[1::2, 1::2] = (f[:-1, :-1] + f[1:, :-1] + f[:-1, 1:] + f[1:, 1:])/4.
1667 diag = f2[1::2, 1::2]
1668 diagA = (f[:-1, :-1] + f[1:, 1:]) / 2.
1669 diagB = (f[1:, :-1] + f[:-1, 1:]) / 2.
1670 f2[1::2, 1::2] = num.where(num.isnan(diag), diagA, diag)
1671 f2[1::2, 1::2] = num.where(num.isnan(diag), diagB, diag)
1672 return f2
1675def doublegrid(x, y, z):
1676 x2 = double1d(x)
1677 y2 = double1d(y)
1678 z2 = double2d(z)
1679 return x2, y2, z2
1682class Guru(object):
1683 '''
1684 Abstract base class providing template interpolation, accessible as
1685 attributes.
1687 Classes deriving from this one, have to implement a :py:meth:`get_params`
1688 method, which is called to get a dict to do ordinary
1689 ``"%(key)x"``-substitutions. The deriving class must also provide a dict
1690 with the templates.
1691 '''
1693 def __init__(self):
1694 self.templates = {}
1696 def fill(self, templates, **kwargs):
1697 params = self.get_params(**kwargs)
1698 strings = [t % params for t in templates]
1699 return strings
1701 # hand through templates dict
1702 def __getitem__(self, template_name):
1703 return self.templates[template_name]
1705 def __setitem__(self, template_name, template):
1706 self.templates[template_name] = template
1708 def __contains__(self, template_name):
1709 return template_name in self.templates
1711 def __iter__(self):
1712 return iter(self.templates)
1714 def __len__(self):
1715 return len(self.templates)
1717 def __delitem__(self, template_name):
1718 del(self.templates[template_name])
1720 def _simple_fill(self, template_names, **kwargs):
1721 templates = [self.templates[n] for n in template_names]
1722 return self.fill(templates, **kwargs)
1724 def __getattr__(self, template_names):
1725 if [n for n in template_names if n not in self.templates]:
1726 raise AttributeError(template_names)
1728 def f(**kwargs):
1729 return self._simple_fill(template_names, **kwargs)
1731 return f
1734def nice_value(x):
1735 '''
1736 Round ``x`` to nice value.
1737 '''
1739 exp = 1.0
1740 sign = 1
1741 if x < 0.0:
1742 x = -x
1743 sign = -1
1744 while x >= 1.0:
1745 x /= 10.0
1746 exp *= 10.0
1747 while x < 0.1:
1748 x *= 10.0
1749 exp /= 10.0
1751 if x >= 0.75:
1752 return sign * 1.0 * exp
1753 if x >= 0.375:
1754 return sign * 0.5 * exp
1755 if x >= 0.225:
1756 return sign * 0.25 * exp
1757 if x >= 0.15:
1758 return sign * 0.2 * exp
1760 return sign * 0.1 * exp
1763class AutoScaler(object):
1764 '''
1765 Tunable 1D autoscaling based on data range.
1767 Instances of this class may be used to determine nice minima, maxima and
1768 increments for ax annotations, as well as suitable common exponents for
1769 notation.
1771 The autoscaling process is guided by the following public attributes:
1773 .. py:attribute:: approx_ticks
1775 Approximate number of increment steps (tickmarks) to generate.
1777 .. py:attribute:: mode
1779 Mode of operation: one of ``'auto'``, ``'min-max'``, ``'0-max'``,
1780 ``'min-0'``, ``'symmetric'`` or ``'off'``.
1782 ================ ==================================================
1783 mode description
1784 ================ ==================================================
1785 ``'auto'``: Look at data range and choose one of the choices
1786 below.
1787 ``'min-max'``: Output range is selected to include data range.
1788 ``'0-max'``: Output range shall start at zero and end at data
1789 max.
1790 ``'min-0'``: Output range shall start at data min and end at
1791 zero.
1792 ``'symmetric'``: Output range shall by symmetric by zero.
1793 ``'off'``: Similar to ``'min-max'``, but snap and space are
1794 disabled, such that the output range always
1795 exactly matches the data range.
1796 ================ ==================================================
1798 .. py:attribute:: exp
1800 If defined, override automatically determined exponent for notation
1801 by the given value.
1803 .. py:attribute:: snap
1805 If set to True, snap output range to multiples of increment. This
1806 parameter has no effect, if mode is set to ``'off'``.
1808 .. py:attribute:: inc
1810 If defined, override automatically determined tick increment by the
1811 given value.
1813 .. py:attribute:: space
1815 Add some padding to the range. The value given, is the fraction by
1816 which the output range is increased on each side. If mode is
1817 ``'0-max'`` or ``'min-0'``, the end at zero is kept fixed at zero.
1818 This parameter has no effect if mode is set to ``'off'``.
1820 .. py:attribute:: exp_factor
1822 Exponent of notation is chosen to be a multiple of this value.
1824 .. py:attribute:: no_exp_interval:
1826 Range of exponent, for which no exponential notation is allowed.
1828 '''
1830 def __init__(
1831 self,
1832 approx_ticks=7.0,
1833 mode='auto',
1834 exp=None,
1835 snap=False,
1836 inc=None,
1837 space=0.0,
1838 exp_factor=3,
1839 no_exp_interval=(-3, 5)):
1841 '''
1842 Create new AutoScaler instance.
1844 The parameters are described in the AutoScaler documentation.
1845 '''
1847 self.approx_ticks = approx_ticks
1848 self.mode = mode
1849 self.exp = exp
1850 self.snap = snap
1851 self.inc = inc
1852 self.space = space
1853 self.exp_factor = exp_factor
1854 self.no_exp_interval = no_exp_interval
1856 def make_scale(self, data_range, override_mode=None):
1858 '''
1859 Get nice minimum, maximum and increment for given data range.
1861 Returns ``(minimum, maximum, increment)`` or ``(maximum, minimum,
1862 -increment)``, depending on whether data_range is ``(data_min,
1863 data_max)`` or ``(data_max, data_min)``. If ``override_mode`` is
1864 defined, the mode attribute is temporarily overridden by the given
1865 value.
1866 '''
1868 data_min = min(data_range)
1869 data_max = max(data_range)
1871 is_reverse = (data_range[0] > data_range[1])
1873 a = self.mode
1874 if self.mode == 'auto':
1875 a = self.guess_autoscale_mode(data_min, data_max)
1877 if override_mode is not None:
1878 a = override_mode
1880 mi, ma = 0, 0
1881 if a == 'off':
1882 mi, ma = data_min, data_max
1883 elif a == '0-max':
1884 mi = 0.0
1885 if data_max > 0.0:
1886 ma = data_max
1887 else:
1888 ma = 1.0
1889 elif a == 'min-0':
1890 ma = 0.0
1891 if data_min < 0.0:
1892 mi = data_min
1893 else:
1894 mi = -1.0
1895 elif a == 'min-max':
1896 mi, ma = data_min, data_max
1897 elif a == 'symmetric':
1898 m = max(abs(data_min), abs(data_max))
1899 mi = -m
1900 ma = m
1902 nmi = mi
1903 if (mi != 0. or a == 'min-max') and a != 'off':
1904 nmi = mi - self.space*(ma-mi)
1906 nma = ma
1907 if (ma != 0. or a == 'min-max') and a != 'off':
1908 nma = ma + self.space*(ma-mi)
1910 mi, ma = nmi, nma
1912 if mi == ma and a != 'off':
1913 mi -= 1.0
1914 ma += 1.0
1916 # make nice tick increment
1917 if self.inc is not None:
1918 inc = self.inc
1919 else:
1920 if self.approx_ticks > 0.:
1921 inc = nice_value((ma-mi) / self.approx_ticks)
1922 else:
1923 inc = nice_value((ma-mi)*10.)
1925 if inc == 0.0:
1926 inc = 1.0
1928 # snap min and max to ticks if this is wanted
1929 if self.snap and a != 'off':
1930 ma = inc * math.ceil(ma/inc)
1931 mi = inc * math.floor(mi/inc)
1933 if is_reverse:
1934 return ma, mi, -inc
1935 else:
1936 return mi, ma, inc
1938 def make_exp(self, x):
1939 '''
1940 Get nice exponent for notation of ``x``.
1942 For ax annotations, give tick increment as ``x``.
1943 '''
1945 if self.exp is not None:
1946 return self.exp
1948 x = abs(x)
1949 if x == 0.0:
1950 return 0
1952 if 10**self.no_exp_interval[0] <= x <= 10**self.no_exp_interval[1]:
1953 return 0
1955 return math.floor(math.log10(x)/self.exp_factor)*self.exp_factor
1957 def guess_autoscale_mode(self, data_min, data_max):
1958 '''
1959 Guess mode of operation, based on data range.
1961 Used to map ``'auto'`` mode to ``'0-max'``, ``'min-0'``, ``'min-max'``
1962 or ``'symmetric'``.
1963 '''
1965 a = 'min-max'
1966 if data_min >= 0.0:
1967 if data_min < data_max/2.:
1968 a = '0-max'
1969 else:
1970 a = 'min-max'
1971 if data_max <= 0.0:
1972 if data_max > data_min/2.:
1973 a = 'min-0'
1974 else:
1975 a = 'min-max'
1976 if data_min < 0.0 and data_max > 0.0:
1977 if abs((abs(data_max)-abs(data_min)) /
1978 (abs(data_max)+abs(data_min))) < 0.5:
1979 a = 'symmetric'
1980 else:
1981 a = 'min-max'
1982 return a
1985class Ax(AutoScaler):
1986 '''
1987 Ax description with autoscaling capabilities.
1989 The ax is described by the :py:class:`AutoScaler` public attributes, plus
1990 the following additional attributes (with default values given in
1991 paranthesis):
1993 .. py:attribute:: label
1995 Ax label (without unit).
1997 .. py:attribute:: unit
1999 Physical unit of the data attached to this ax.
2001 .. py:attribute:: scaled_unit
2003 (see below)
2005 .. py:attribute:: scaled_unit_factor
2007 Scaled physical unit and factor between unit and scaled_unit so that
2009 unit = scaled_unit_factor x scaled_unit.
2011 (E.g. if unit is 'm' and data is in the range of nanometers, you may
2012 want to set the scaled_unit to 'nm' and the scaled_unit_factor to
2013 1e9.)
2015 .. py:attribute:: limits
2017 If defined, fix range of ax to limits=(min,max).
2019 .. py:attribute:: masking
2021 If true and if there is a limit on the ax, while calculating ranges,
2022 the data points are masked such that data points outside of this axes
2023 limits are not used to determine the range of another dependant ax.
2025 '''
2027 def __init__(self, label='', unit='', scaled_unit_factor=1.,
2028 scaled_unit='', limits=None, masking=True, **kwargs):
2030 AutoScaler.__init__(self, **kwargs)
2031 self.label = label
2032 self.unit = unit
2033 self.scaled_unit_factor = scaled_unit_factor
2034 self.scaled_unit = scaled_unit
2035 self.limits = limits
2036 self.masking = masking
2038 def label_str(self, exp, unit):
2039 '''
2040 Get label string including the unit and multiplier.
2041 '''
2043 slabel, sunit, sexp = '', '', ''
2044 if self.label:
2045 slabel = self.label
2047 if unit or exp != 0:
2048 if exp != 0:
2049 sexp = '\\327 10@+%i@+' % exp
2050 sunit = '[ %s %s ]' % (sexp, unit)
2051 else:
2052 sunit = '[ %s ]' % unit
2054 p = []
2055 if slabel:
2056 p.append(slabel)
2058 if sunit:
2059 p.append(sunit)
2061 return ' '.join(p)
2063 def make_params(self, data_range, ax_projection=False, override_mode=None,
2064 override_scaled_unit_factor=None):
2066 '''
2067 Get minimum, maximum, increment and label string for ax display.'
2069 Returns minimum, maximum, increment and label string including unit and
2070 multiplier for given data range.
2072 If ``ax_projection`` is True, values suitable to be displayed on the ax
2073 are returned, e.g. min, max and inc are returned in scaled units.
2074 Otherwise the values are returned in the original units, without any
2075 scaling applied.
2076 '''
2078 sf = self.scaled_unit_factor
2080 if override_scaled_unit_factor is not None:
2081 sf = override_scaled_unit_factor
2083 dr_scaled = [sf*x for x in data_range]
2085 mi, ma, inc = self.make_scale(dr_scaled, override_mode=override_mode)
2086 if self.inc is not None:
2087 inc = self.inc*sf
2089 if ax_projection:
2090 exp = self.make_exp(inc)
2091 if sf == 1. and override_scaled_unit_factor is None:
2092 unit = self.unit
2093 else:
2094 unit = self.scaled_unit
2095 label = self.label_str(exp, unit)
2096 return mi/10**exp, ma/10**exp, inc/10**exp, label
2097 else:
2098 label = self.label_str(0, self.unit)
2099 return mi/sf, ma/sf, inc/sf, label
2102class ScaleGuru(Guru):
2104 '''
2105 2D/3D autoscaling and ax annotation facility.
2107 Instances of this class provide automatic determination of plot ranges,
2108 tick increments and scaled annotations, as well as label/unit handling. It
2109 can in particular be used to automatically generate the -R and -B option
2110 arguments, which are required for most GMT commands.
2112 It extends the functionality of the :py:class:`Ax` and
2113 :py:class:`AutoScaler` classes at the level, where it can not be handled
2114 anymore by looking at a single dimension of the dataset's data, e.g.:
2116 * The ability to impose a fixed aspect ratio between two axes.
2118 * Recalculation of data range on non-limited axes, when there are
2119 limits imposed on other axes.
2121 '''
2123 def __init__(self, data_tuples=None, axes=None, aspect=None,
2124 percent_interval=None, copy_from=None):
2126 Guru.__init__(self)
2128 if copy_from:
2129 self.templates = copy.deepcopy(copy_from.templates)
2130 self.axes = copy.deepcopy(copy_from.axes)
2131 self.data_ranges = copy.deepcopy(copy_from.data_ranges)
2132 self.aspect = copy_from.aspect
2134 if percent_interval is not None:
2135 from scipy.stats import scoreatpercentile as scap
2137 self.templates = dict(
2138 R='-R%(xmin)g/%(xmax)g/%(ymin)g/%(ymax)g',
2139 B='-B%(xinc)g:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen',
2140 T='-T%(zmin)g/%(zmax)g/%(zinc)g')
2142 maxdim = 2
2143 if data_tuples:
2144 maxdim = max(maxdim, max([len(dt) for dt in data_tuples]))
2145 else:
2146 if axes:
2147 maxdim = len(axes)
2148 data_tuples = [([],) * maxdim]
2149 if axes is not None:
2150 self.axes = axes
2151 else:
2152 self.axes = [Ax() for i in range(maxdim)]
2154 # sophisticated data-range calculation
2155 data_ranges = [None] * maxdim
2156 for dt_ in data_tuples:
2157 dt = num.asarray(dt_)
2158 in_range = True
2159 for ax, x in zip(self.axes, dt):
2160 if ax.limits and ax.masking:
2161 ax_limits = list(ax.limits)
2162 if ax_limits[0] is None:
2163 ax_limits[0] = -num.inf
2164 if ax_limits[1] is None:
2165 ax_limits[1] = num.inf
2166 in_range = num.logical_and(
2167 in_range,
2168 num.logical_and(ax_limits[0] <= x, x <= ax_limits[1]))
2170 for i, ax, x in zip(range(maxdim), self.axes, dt):
2172 if not ax.limits or None in ax.limits:
2173 if len(x) >= 1:
2174 if in_range is not True:
2175 xmasked = num.where(in_range, x, num.NaN)
2176 if percent_interval is None:
2177 range_this = (
2178 num.nanmin(xmasked),
2179 num.nanmax(xmasked))
2180 else:
2181 xmasked_finite = num.compress(
2182 num.isfinite(xmasked), xmasked)
2183 range_this = (
2184 scap(xmasked_finite,
2185 (100.-percent_interval)/2.),
2186 scap(xmasked_finite,
2187 100.-(100.-percent_interval)/2.))
2188 else:
2189 if percent_interval is None:
2190 range_this = num.nanmin(x), num.nanmax(x)
2191 else:
2192 xmasked_finite = num.compress(
2193 num.isfinite(xmasked), xmasked)
2194 range_this = (
2195 scap(xmasked_finite,
2196 (100.-percent_interval)/2.),
2197 scap(xmasked_finite,
2198 100.-(100.-percent_interval)/2.))
2199 else:
2200 range_this = (0., 1.)
2202 if ax.limits:
2203 if ax.limits[0] is not None:
2204 range_this = ax.limits[0], max(ax.limits[0],
2205 range_this[1])
2207 if ax.limits[1] is not None:
2208 range_this = min(ax.limits[1],
2209 range_this[0]), ax.limits[1]
2211 else:
2212 range_this = ax.limits
2214 if data_ranges[i] is None and range_this[0] <= range_this[1]:
2215 data_ranges[i] = range_this
2216 else:
2217 mi, ma = range_this
2218 if data_ranges[i] is not None:
2219 mi = min(data_ranges[i][0], mi)
2220 ma = max(data_ranges[i][1], ma)
2222 data_ranges[i] = (mi, ma)
2224 for i in range(len(data_ranges)):
2225 if data_ranges[i] is None or not (
2226 num.isfinite(data_ranges[i][0])
2227 and num.isfinite(data_ranges[i][1])):
2229 data_ranges[i] = (0., 1.)
2231 self.data_ranges = data_ranges
2232 self.aspect = aspect
2234 def copy(self):
2235 return ScaleGuru(copy_from=self)
2237 def get_params(self, ax_projection=False):
2239 '''
2240 Get dict with output parameters.
2242 For each data dimension, ax minimum, maximum, increment and a label
2243 string (including unit and exponential factor) are determined. E.g. in
2244 for the first dimension the output dict will contain the keys
2245 ``'xmin'``, ``'xmax'``, ``'xinc'``, and ``'xlabel'``.
2247 Normally, values corresponding to the scaling of the raw data are
2248 produced, but if ``ax_projection`` is ``True``, values which are
2249 suitable to be printed on the axes are returned. This means that in the
2250 latter case, the :py:attr:`Ax.scaled_unit` and
2251 :py:attr:`Ax.scaled_unit_factor` attributes as set on the axes are
2252 respected and that a common 10^x factor is factored out and put to the
2253 label string.
2254 '''
2256 xmi, xma, xinc, xlabel = self.axes[0].make_params(
2257 self.data_ranges[0], ax_projection)
2258 ymi, yma, yinc, ylabel = self.axes[1].make_params(
2259 self.data_ranges[1], ax_projection)
2260 if len(self.axes) > 2:
2261 zmi, zma, zinc, zlabel = self.axes[2].make_params(
2262 self.data_ranges[2], ax_projection)
2264 # enforce certain aspect, if needed
2265 if self.aspect is not None:
2266 xwid = xma-xmi
2267 ywid = yma-ymi
2268 if ywid < xwid*self.aspect:
2269 ymi -= (xwid*self.aspect - ywid)*0.5
2270 yma += (xwid*self.aspect - ywid)*0.5
2271 ymi, yma, yinc, ylabel = self.axes[1].make_params(
2272 (ymi, yma), ax_projection, override_mode='off',
2273 override_scaled_unit_factor=1.)
2275 elif xwid < ywid/self.aspect:
2276 xmi -= (ywid/self.aspect - xwid)*0.5
2277 xma += (ywid/self.aspect - xwid)*0.5
2278 xmi, xma, xinc, xlabel = self.axes[0].make_params(
2279 (xmi, xma), ax_projection, override_mode='off',
2280 override_scaled_unit_factor=1.)
2282 params = dict(xmin=xmi, xmax=xma, xinc=xinc, xlabel=xlabel,
2283 ymin=ymi, ymax=yma, yinc=yinc, ylabel=ylabel)
2284 if len(self.axes) > 2:
2285 params.update(dict(zmin=zmi, zmax=zma, zinc=zinc, zlabel=zlabel))
2287 return params
2290class GumSpring(object):
2292 '''
2293 Sizing policy implementing a minimal size, plus a desire to grow.
2294 '''
2296 def __init__(self, minimal=None, grow=None):
2297 self.minimal = minimal
2298 if grow is None:
2299 if minimal is None:
2300 self.grow = 1.0
2301 else:
2302 self.grow = 0.0
2303 else:
2304 self.grow = grow
2305 self.value = 1.0
2307 def get_minimal(self):
2308 if self.minimal is not None:
2309 return self.minimal
2310 else:
2311 return 0.0
2313 def get_grow(self):
2314 return self.grow
2316 def set_value(self, value):
2317 self.value = value
2319 def get_value(self):
2320 return self.value
2323def distribute(sizes, grows, space):
2324 sizes = list(sizes)
2325 gsum = sum(grows)
2326 if gsum > 0.0:
2327 for i in range(len(sizes)):
2328 sizes[i] += space*grows[i]/gsum
2329 return sizes
2332class Widget(Guru):
2334 '''
2335 Base class of the gmtpy layout system.
2337 The Widget class provides the basic functionality for the nesting and
2338 placing of elements on the output page, and maintains the sizing policies
2339 of each element. Each of the layouts defined in gmtpy is itself a Widget.
2341 Sizing of the widget is controlled by :py:meth:`get_min_size` and
2342 :py:meth:`get_grow` which should be overloaded in derived classes. The
2343 basic behaviour of a Widget instance is to have a vertical and a horizontal
2344 minimum size which default to zero, as well as a vertical and a horizontal
2345 desire to grow, represented by floats, which default to 1.0. Additionally
2346 an aspect ratio constraint may be imposed on the Widget.
2348 After layouting, the widget provides its width, height, x-offset and
2349 y-offset in various ways. Via the Guru interface (see :py:class:`Guru`
2350 class), templates for the -X, -Y and -J option arguments used by GMT
2351 arguments are provided. The defaults are suitable for plotting of linear
2352 (-JX) plots. Other projections can be selected by giving an appropriate 'J'
2353 template, or by manual construction of the -J option, e.g. by utilizing the
2354 :py:meth:`width` and :py:meth:`height` methods. The :py:meth:`bbox` method
2355 can be used to create a PostScript bounding box from the widgets border,
2356 e.g. for use in the :py:meth:`save` method of :py:class:`GMT` instances.
2358 The convention is, that all sizes are given in PostScript points.
2359 Conversion factors are provided as constants :py:const:`inch` and
2360 :py:const:`cm` in the gmtpy module.
2361 '''
2363 def __init__(self, horizontal=None, vertical=None, parent=None):
2365 '''
2366 Create new widget.
2367 '''
2369 Guru.__init__(self)
2371 self.templates = dict(
2372 X='-Xa%(xoffset)gp',
2373 Y='-Ya%(yoffset)gp',
2374 J='-JX%(width)gp/%(height)gp')
2376 if horizontal is None:
2377 self.horizontal = GumSpring()
2378 else:
2379 self.horizontal = horizontal
2381 if vertical is None:
2382 self.vertical = GumSpring()
2383 else:
2384 self.vertical = vertical
2386 self.aspect = None
2387 self.parent = parent
2388 self.dirty = True
2390 def set_parent(self, parent):
2392 '''
2393 Set the parent widget.
2395 This method should not be called directly. The :py:meth:`set_widget`
2396 methods are responsible for calling this.
2397 '''
2399 self.parent = parent
2400 self.dirtyfy()
2402 def get_parent(self):
2404 '''
2405 Get the widgets parent widget.
2406 '''
2408 return self.parent
2410 def get_root(self):
2412 '''
2413 Get the root widget in the layout hierarchy.
2414 '''
2416 if self.parent is not None:
2417 return self.get_parent()
2418 else:
2419 return self
2421 def set_horizontal(self, minimal=None, grow=None):
2423 '''
2424 Set the horizontal sizing policy of the Widget.
2427 :param minimal: new minimal width of the widget
2428 :param grow: new horizontal grow disire of the widget
2429 '''
2431 self.horizontal = GumSpring(minimal, grow)
2432 self.dirtyfy()
2434 def get_horizontal(self):
2435 return self.horizontal.get_minimal(), self.horizontal.get_grow()
2437 def set_vertical(self, minimal=None, grow=None):
2439 '''
2440 Set the horizontal sizing policy of the Widget.
2442 :param minimal: new minimal height of the widget
2443 :param grow: new vertical grow disire of the widget
2444 '''
2446 self.vertical = GumSpring(minimal, grow)
2447 self.dirtyfy()
2449 def get_vertical(self):
2450 return self.vertical.get_minimal(), self.vertical.get_grow()
2452 def set_aspect(self, aspect=None):
2454 '''
2455 Set aspect constraint on the widget.
2457 The aspect is given as height divided by width.
2458 '''
2460 self.aspect = aspect
2461 self.dirtyfy()
2463 def set_policy(self, minimal=(None, None), grow=(None, None), aspect=None):
2465 '''
2466 Shortcut to set sizing and aspect constraints in a single method
2467 call.
2468 '''
2470 self.set_horizontal(minimal[0], grow[0])
2471 self.set_vertical(minimal[1], grow[1])
2472 self.set_aspect(aspect)
2474 def get_policy(self):
2475 mh, gh = self.get_horizontal()
2476 mv, gv = self.get_vertical()
2477 return (mh, mv), (gh, gv), self.aspect
2479 def legalize(self, size, offset):
2481 '''
2482 Get legal size for widget.
2484 Returns: (new_size, new_offset)
2486 Given a box as ``size`` and ``offset``, return ``new_size`` and
2487 ``new_offset``, such that the widget's sizing and aspect constraints
2488 are fullfilled. The returned box is centered on the given input box.
2489 '''
2491 sh, sv = size
2492 oh, ov = offset
2493 shs, svs = Widget.get_min_size(self)
2494 ghs, gvs = Widget.get_grow(self)
2496 if ghs == 0.0:
2497 oh += (sh-shs)/2.
2498 sh = shs
2500 if gvs == 0.0:
2501 ov += (sv-svs)/2.
2502 sv = svs
2504 if self.aspect is not None:
2505 if sh > sv/self.aspect:
2506 oh += (sh-sv/self.aspect)/2.
2507 sh = sv/self.aspect
2508 if sv > sh*self.aspect:
2509 ov += (sv-sh*self.aspect)/2.
2510 sv = sh*self.aspect
2512 return (sh, sv), (oh, ov)
2514 def get_min_size(self):
2516 '''
2517 Get minimum size of widget.
2519 Used by the layout managers. Should be overloaded in derived classes.
2520 '''
2522 mh, mv = self.horizontal.get_minimal(), self.vertical.get_minimal()
2523 if self.aspect is not None:
2524 if mv == 0.0:
2525 return mh, mh*self.aspect
2526 elif mh == 0.0:
2527 return mv/self.aspect, mv
2528 return mh, mv
2530 def get_grow(self):
2532 '''
2533 Get widget's desire to grow.
2535 Used by the layout managers. Should be overloaded in derived classes.
2536 '''
2538 return self.horizontal.get_grow(), self.vertical.get_grow()
2540 def set_size(self, size, offset):
2542 '''
2543 Set the widget's current size.
2545 Should not be called directly. It is the layout manager's
2546 responsibility to call this.
2547 '''
2549 (sh, sv), inner_offset = self.legalize(size, offset)
2550 self.offset = inner_offset
2551 self.horizontal.set_value(sh)
2552 self.vertical.set_value(sv)
2553 self.dirty = False
2555 def __str__(self):
2557 def indent(ind, str):
2558 return ('\n'+ind).join(str.splitlines())
2559 size, offset = self.get_size()
2560 s = "%s (%g x %g) (%g, %g)\n" % ((self.__class__,) + size + offset)
2561 children = self.get_children()
2562 if children:
2563 s += '\n'.join([' ' + indent(' ', str(c)) for c in children])
2564 return s
2566 def policies_debug_str(self):
2568 def indent(ind, str):
2569 return ('\n'+ind).join(str.splitlines())
2570 mins, grows, aspect = self.get_policy()
2571 s = "%s: minimum=(%s, %s), grow=(%s, %s), aspect=%s\n" % (
2572 (self.__class__,) + mins+grows+(aspect,))
2574 children = self.get_children()
2575 if children:
2576 s += '\n'.join([' ' + indent(
2577 ' ', c.policies_debug_str()) for c in children])
2578 return s
2580 def get_corners(self, descend=False):
2582 '''
2583 Get coordinates of the corners of the widget.
2585 Returns list with coordinate tuples.
2587 If ``descend`` is True, the returned list will contain corner
2588 coordinates of all sub-widgets.
2589 '''
2591 self.do_layout()
2592 (sh, sv), (oh, ov) = self.get_size()
2593 corners = [(oh, ov), (oh+sh, ov), (oh+sh, ov+sv), (oh, ov+sv)]
2594 if descend:
2595 for child in self.get_children():
2596 corners.extend(child.get_corners(descend=True))
2597 return corners
2599 def get_sizes(self):
2601 '''
2602 Get sizes of this widget and all it's children.
2604 Returns a list with size tuples.
2605 '''
2606 self.do_layout()
2607 sizes = [self.get_size()]
2608 for child in self.get_children():
2609 sizes.extend(child.get_sizes())
2610 return sizes
2612 def do_layout(self):
2614 '''
2615 Triggers layouting of the widget hierarchy, if needed.
2616 '''
2618 if self.parent is not None:
2619 return self.parent.do_layout()
2621 if not self.dirty:
2622 return
2624 sh, sv = self.get_min_size()
2625 gh, gv = self.get_grow()
2626 if sh == 0.0 and gh != 0.0:
2627 sh = 15.*cm
2628 if sv == 0.0 and gv != 0.0:
2629 sv = 15.*cm*gv/gh * 1./golden_ratio
2630 self.set_size((sh, sv), (0., 0.))
2632 def get_children(self):
2634 '''
2635 Get sub-widgets contained in this widget.
2637 Returns a list of widgets.
2638 '''
2640 return []
2642 def get_size(self):
2644 '''
2645 Get current size and position of the widget.
2647 Triggers layouting and returns
2648 ``((width, height), (xoffset, yoffset))``
2649 '''
2651 self.do_layout()
2652 return (self.horizontal.get_value(),
2653 self.vertical.get_value()), self.offset
2655 def get_params(self):
2657 '''
2658 Get current size and position of the widget.
2660 Triggers layouting and returns dict with keys ``'xoffset'``,
2661 ``'yoffset'``, ``'width'`` and ``'height'``.
2662 '''
2664 self.do_layout()
2665 (w, h), (xo, yo) = self.get_size()
2666 return dict(xoffset=xo, yoffset=yo, width=w, height=h,
2667 width_m=w/_units['m'])
2669 def width(self):
2671 '''
2672 Get current width of the widget.
2674 Triggers layouting and returns width.
2675 '''
2677 self.do_layout()
2678 return self.horizontal.get_value()
2680 def height(self):
2682 '''
2683 Get current height of the widget.
2685 Triggers layouting and return height.
2686 '''
2688 self.do_layout()
2689 return self.vertical.get_value()
2691 def bbox(self):
2693 '''
2694 Get PostScript bounding box for this widget.
2696 Triggers layouting and returns values suitable to create PS bounding
2697 box, representing the widgets current size and position.
2698 '''
2700 self.do_layout()
2701 return (self.offset[0], self.offset[1], self.offset[0]+self.width(),
2702 self.offset[1]+self.height())
2704 def dirtyfy(self):
2706 '''
2707 Set dirty flag on top level widget in the hierarchy.
2709 Called by various methods, to indicate, that the widget hierarchy needs
2710 new layouting.
2711 '''
2713 if self.parent is not None:
2714 self.parent.dirtyfy()
2716 self.dirty = True
2719class CenterLayout(Widget):
2721 '''
2722 A layout manager which centers its single child widget.
2724 The child widget may be oversized.
2725 '''
2727 def __init__(self, horizontal=None, vertical=None):
2728 Widget.__init__(self, horizontal, vertical)
2729 self.content = Widget(horizontal=GumSpring(grow=1.),
2730 vertical=GumSpring(grow=1.), parent=self)
2732 def get_min_size(self):
2733 shs, svs = Widget.get_min_size(self)
2734 sh, sv = self.content.get_min_size()
2735 return max(shs, sh), max(svs, sv)
2737 def get_grow(self):
2738 ghs, gvs = Widget.get_grow(self)
2739 gh, gv = self.content.get_grow()
2740 return gh*ghs, gv*gvs
2742 def set_size(self, size, offset):
2743 (sh, sv), (oh, ov) = self.legalize(size, offset)
2745 shc, svc = self.content.get_min_size()
2746 ghc, gvc = self.content.get_grow()
2747 if ghc != 0.:
2748 shc = sh
2749 if gvc != 0.:
2750 svc = sv
2751 ohc = oh+(sh-shc)/2.
2752 ovc = ov+(sv-svc)/2.
2754 self.content.set_size((shc, svc), (ohc, ovc))
2755 Widget.set_size(self, (sh, sv), (oh, ov))
2757 def set_widget(self, widget=None):
2759 '''
2760 Set the child widget, which shall be centered.
2761 '''
2763 if widget is None:
2764 widget = Widget()
2766 self.content = widget
2768 widget.set_parent(self)
2770 def get_widget(self):
2771 return self.content
2773 def get_children(self):
2774 return [self.content]
2777class FrameLayout(Widget):
2779 '''
2780 A layout manager containing a center widget sorrounded by four margin
2781 widgets.
2783 ::
2785 +---------------------------+
2786 | top |
2787 +---------------------------+
2788 | | | |
2789 | left | center | right |
2790 | | | |
2791 +---------------------------+
2792 | bottom |
2793 +---------------------------+
2795 This layout manager does a little bit of extra effort to maintain the
2796 aspect constraint of the center widget, if this is set. It does so, by
2797 allowing for a bit more flexibility in the sizing of the margins. Two
2798 shortcut methods are provided to set the margin sizes in one shot:
2799 :py:meth:`set_fixed_margins` and :py:meth:`set_min_margins`. The first sets
2800 the margins to fixed sizes, while the second gives them a minimal size and
2801 a (neglectably) small desire to grow. Using the latter may be useful when
2802 setting an aspect constraint on the center widget, because this way the
2803 maximum size of the center widget may be controlled without creating empty
2804 spaces between the widgets.
2805 '''
2807 def __init__(self, horizontal=None, vertical=None):
2808 Widget.__init__(self, horizontal, vertical)
2809 mw = 3.*cm
2810 self.left = Widget(
2811 horizontal=GumSpring(grow=0.15, minimal=mw), parent=self)
2812 self.right = Widget(
2813 horizontal=GumSpring(grow=0.15, minimal=mw), parent=self)
2814 self.top = Widget(
2815 vertical=GumSpring(grow=0.15, minimal=mw/golden_ratio),
2816 parent=self)
2817 self.bottom = Widget(
2818 vertical=GumSpring(grow=0.15, minimal=mw/golden_ratio),
2819 parent=self)
2820 self.center = Widget(
2821 horizontal=GumSpring(grow=0.7), vertical=GumSpring(grow=0.7),
2822 parent=self)
2824 def set_fixed_margins(self, left, right, top, bottom):
2825 '''
2826 Give margins fixed size constraints.
2827 '''
2829 self.left.set_horizontal(left, 0)
2830 self.right.set_horizontal(right, 0)
2831 self.top.set_vertical(top, 0)
2832 self.bottom.set_vertical(bottom, 0)
2834 def set_min_margins(self, left, right, top, bottom, grow=0.0001):
2835 '''
2836 Give margins a minimal size and the possibility to grow.
2838 The desire to grow is set to a very small number.
2839 '''
2840 self.left.set_horizontal(left, grow)
2841 self.right.set_horizontal(right, grow)
2842 self.top.set_vertical(top, grow)
2843 self.bottom.set_vertical(bottom, grow)
2845 def get_min_size(self):
2846 shs, svs = Widget.get_min_size(self)
2848 sl, sr, st, sb, sc = [x.get_min_size() for x in (
2849 self.left, self.right, self.top, self.bottom, self.center)]
2850 gl, gr, gt, gb, gc = [x.get_grow() for x in (
2851 self.left, self.right, self.top, self.bottom, self.center)]
2853 shsum = sl[0]+sr[0]+sc[0]
2854 svsum = st[1]+sb[1]+sc[1]
2856 # prevent widgets from collapsing
2857 for s, g in ((sl, gl), (sr, gr), (sc, gc)):
2858 if s[0] == 0.0 and g[0] != 0.0:
2859 shsum += 0.1*cm
2861 for s, g in ((st, gt), (sb, gb), (sc, gc)):
2862 if s[1] == 0.0 and g[1] != 0.0:
2863 svsum += 0.1*cm
2865 sh = max(shs, shsum)
2866 sv = max(svs, svsum)
2868 return sh, sv
2870 def get_grow(self):
2871 ghs, gvs = Widget.get_grow(self)
2872 gh = (self.left.get_grow()[0] +
2873 self.right.get_grow()[0] +
2874 self.center.get_grow()[0]) * ghs
2875 gv = (self.top.get_grow()[1] +
2876 self.bottom.get_grow()[1] +
2877 self.center.get_grow()[1]) * gvs
2878 return gh, gv
2880 def set_size(self, size, offset):
2881 (sh, sv), (oh, ov) = self.legalize(size, offset)
2883 sl, sr, st, sb, sc = [x.get_min_size() for x in (
2884 self.left, self.right, self.top, self.bottom, self.center)]
2885 gl, gr, gt, gb, gc = [x.get_grow() for x in (
2886 self.left, self.right, self.top, self.bottom, self.center)]
2888 ah = sh - (sl[0]+sr[0]+sc[0])
2889 av = sv - (st[1]+sb[1]+sc[1])
2891 if ah < 0.0:
2892 raise GmtPyError("Container not wide enough for contents "
2893 "(FrameLayout, available: %g cm, needed: %g cm)"
2894 % (sh/cm, (sl[0]+sr[0]+sc[0])/cm))
2895 if av < 0.0:
2896 raise GmtPyError("Container not high enough for contents "
2897 "(FrameLayout, available: %g cm, needed: %g cm)"
2898 % (sv/cm, (st[1]+sb[1]+sc[1])/cm))
2900 slh, srh, sch = distribute((sl[0], sr[0], sc[0]),
2901 (gl[0], gr[0], gc[0]), ah)
2902 stv, sbv, scv = distribute((st[1], sb[1], sc[1]),
2903 (gt[1], gb[1], gc[1]), av)
2905 if self.center.aspect is not None:
2906 ahm = sh - (sl[0]+sr[0] + scv/self.center.aspect)
2907 avm = sv - (st[1]+sb[1] + sch*self.center.aspect)
2908 if 0.0 < ahm < ah:
2909 slh, srh, sch = distribute(
2910 (sl[0], sr[0], scv/self.center.aspect),
2911 (gl[0], gr[0], 0.0), ahm)
2913 elif 0.0 < avm < av:
2914 stv, sbv, scv = distribute((st[1], sb[1],
2915 sch*self.center.aspect),
2916 (gt[1], gb[1], 0.0), avm)
2918 ah = sh - (slh+srh+sch)
2919 av = sv - (stv+sbv+scv)
2921 oh += ah/2.
2922 ov += av/2.
2923 sh -= ah
2924 sv -= av
2926 self.left.set_size((slh, scv), (oh, ov+sbv))
2927 self.right.set_size((srh, scv), (oh+slh+sch, ov+sbv))
2928 self.top.set_size((sh, stv), (oh, ov+sbv+scv))
2929 self.bottom.set_size((sh, sbv), (oh, ov))
2930 self.center.set_size((sch, scv), (oh+slh, ov+sbv))
2931 Widget.set_size(self, (sh, sv), (oh, ov))
2933 def set_widget(self, which='center', widget=None):
2935 '''
2936 Set one of the sub-widgets.
2938 ``which`` should be one of ``'left'``, ``'right'``, ``'top'``,
2939 ``'bottom'`` or ``'center'``.
2940 '''
2942 if widget is None:
2943 widget = Widget()
2945 if which in ('left', 'right', 'top', 'bottom', 'center'):
2946 self.__dict__[which] = widget
2947 else:
2948 raise GmtPyError('No such sub-widget: %s' % which)
2950 widget.set_parent(self)
2952 def get_widget(self, which='center'):
2954 '''
2955 Get one of the sub-widgets.
2957 ``which`` should be one of ``'left'``, ``'right'``, ``'top'``,
2958 ``'bottom'`` or ``'center'``.
2959 '''
2961 if which in ('left', 'right', 'top', 'bottom', 'center'):
2962 return self.__dict__[which]
2963 else:
2964 raise GmtPyError('No such sub-widget: %s' % which)
2966 def get_children(self):
2967 return [self.left, self.right, self.top, self.bottom, self.center]
2970class GridLayout(Widget):
2972 '''
2973 A layout manager which arranges its sub-widgets in a grid.
2975 The grid spacing is flexible and based on the sizing policies of the
2976 contained sub-widgets. If an equidistant grid is needed, the sizing
2977 policies of the sub-widgets have to be set equally.
2979 The height of each row and the width of each column is derived from the
2980 sizing policy of the largest sub-widget in the row or column in question.
2981 The algorithm is not very sophisticated, so conflicting sizing policies
2982 might not be resolved optimally.
2983 '''
2985 def __init__(self, nx=2, ny=2, horizontal=None, vertical=None):
2987 '''
2988 Create new grid layout with ``nx`` columns and ``ny`` rows.
2989 '''
2991 Widget.__init__(self, horizontal, vertical)
2992 self.grid = []
2993 for iy in range(ny):
2994 row = []
2995 for ix in range(nx):
2996 w = Widget(parent=self)
2997 row.append(w)
2999 self.grid.append(row)
3001 def sub_min_sizes_as_array(self):
3002 esh = num.array(
3003 [[w.get_min_size()[0] for w in row] for row in self.grid],
3004 dtype=float)
3005 esv = num.array(
3006 [[w.get_min_size()[1] for w in row] for row in self.grid],
3007 dtype=float)
3008 return esh, esv
3010 def sub_grows_as_array(self):
3011 egh = num.array(
3012 [[w.get_grow()[0] for w in row] for row in self.grid],
3013 dtype=float)
3014 egv = num.array(
3015 [[w.get_grow()[1] for w in row] for row in self.grid],
3016 dtype=float)
3017 return egh, egv
3019 def get_min_size(self):
3020 sh, sv = Widget.get_min_size(self)
3021 esh, esv = self.sub_min_sizes_as_array()
3022 if esh.size != 0:
3023 sh = max(sh, num.sum(esh.max(0)))
3024 if esv.size != 0:
3025 sv = max(sv, num.sum(esv.max(1)))
3026 return sh, sv
3028 def get_grow(self):
3029 ghs, gvs = Widget.get_grow(self)
3030 egh, egv = self.sub_grows_as_array()
3031 if egh.size != 0:
3032 gh = num.sum(egh.max(0))*ghs
3033 else:
3034 gh = 1.0
3035 if egv.size != 0:
3036 gv = num.sum(egv.max(1))*gvs
3037 else:
3038 gv = 1.0
3039 return gh, gv
3041 def set_size(self, size, offset):
3042 (sh, sv), (oh, ov) = self.legalize(size, offset)
3043 esh, esv = self.sub_min_sizes_as_array()
3044 egh, egv = self.sub_grows_as_array()
3046 # available additional space
3047 empty = esh.size == 0
3049 if not empty:
3050 ah = sh - num.sum(esh.max(0))
3051 av = sv - num.sum(esv.max(1))
3052 else:
3053 av = sv
3054 ah = sh
3056 if ah < 0.0:
3057 raise GmtPyError("Container not wide enough for contents "
3058 "(GridLayout, available: %g cm, needed: %g cm)"
3059 % (sh/cm, (num.sum(esh.max(0)))/cm))
3060 if av < 0.0:
3061 raise GmtPyError("Container not high enough for contents "
3062 "(GridLayout, available: %g cm, needed: %g cm)"
3063 % (sv/cm, (num.sum(esv.max(1)))/cm))
3065 nx, ny = esh.shape
3067 if not empty:
3068 # distribute additional space on rows and columns
3069 # according to grow weights and minimal sizes
3070 gsh = egh.sum(1)[:, num.newaxis].repeat(ny, axis=1)
3071 nesh = esh.copy()
3072 nesh += num.where(gsh > 0.0, ah*egh/gsh, 0.0)
3074 nsh = num.maximum(nesh.max(0), esh.max(0))
3076 gsv = egv.sum(0)[num.newaxis, :].repeat(nx, axis=0)
3077 nesv = esv.copy()
3078 nesv += num.where(gsv > 0.0, av*egv/gsv, 0.0)
3079 nsv = num.maximum(nesv.max(1), esv.max(1))
3081 ah = sh - sum(nsh)
3082 av = sv - sum(nsv)
3084 oh += ah/2.
3085 ov += av/2.
3086 sh -= ah
3087 sv -= av
3089 # resize child widgets
3090 neov = ov + sum(nsv)
3091 for row, nesv in zip(self.grid, nsv):
3092 neov -= nesv
3093 neoh = oh
3094 for w, nesh in zip(row, nsh):
3095 w.set_size((nesh, nesv), (neoh, neov))
3096 neoh += nesh
3098 Widget.set_size(self, (sh, sv), (oh, ov))
3100 def set_widget(self, ix, iy, widget=None):
3102 '''
3103 Set one of the sub-widgets.
3105 Sets the sub-widget in column ``ix`` and row ``iy``. The indices are
3106 counted from zero.
3107 '''
3109 if widget is None:
3110 widget = Widget()
3112 self.grid[iy][ix] = widget
3113 widget.set_parent(self)
3115 def get_widget(self, ix, iy):
3117 '''
3118 Get one of the sub-widgets.
3120 Gets the sub-widget from column ``ix`` and row ``iy``. The indices are
3121 counted from zero.
3122 '''
3124 return self.grid[iy][ix]
3126 def get_children(self):
3127 children = []
3128 for row in self.grid:
3129 children.extend(row)
3131 return children
3134def is_gmt5(version='newest'):
3135 return get_gmt_installation(version)['version'][0] == '5'
3138def aspect_for_projection(gmtversion, *args, **kwargs):
3140 gmt = GMT(version=gmtversion, eps_mode=True)
3142 if gmt.is_gmt5():
3143 gmt.psbasemap('-B+gblack', finish=True, *args, **kwargs)
3144 fn = gmt.tempfilename('test.eps')
3145 gmt.save(fn, crop_eps_mode=True)
3146 with open(fn, 'rb') as f:
3147 s = f.read()
3149 l, b, r, t = get_bbox(s)
3150 else:
3151 gmt.psbasemap('-G0', finish=True, *args, **kwargs)
3152 l, b, r, t = gmt.bbox()
3154 return (t-b)/(r-l)
3157def text_box(
3158 text, font=0, font_size=12., angle=0, gmtversion='newest', **kwargs):
3160 gmt = GMT(version=gmtversion)
3161 if gmt.is_gmt5():
3162 row = [0, 0, text]
3163 farg = ['-F+f%gp,%s,%s+j%s' % (font_size, font, 'black', 'BL')]
3164 else:
3165 row = [0, 0, font_size, 0, font, 'BL', text]
3166 farg = []
3168 gmt.pstext(
3169 in_rows=[row],
3170 finish=True,
3171 R=(0, 1, 0, 1),
3172 J='x10p',
3173 N=True,
3174 *farg,
3175 **kwargs)
3177 fn = gmt.tempfilename() + '.ps'
3178 gmt.save(fn)
3180 (_, stderr) = subprocess.Popen(
3181 ['gs', '-q', '-dNOPAUSE', '-dBATCH', '-r720', '-sDEVICE=bbox', fn],
3182 stderr=subprocess.PIPE).communicate()
3184 dx, dy = None, None
3185 for line in stderr.splitlines():
3186 if line.startswith(b'%%HiResBoundingBox:'):
3187 l, b, r, t = [float(x) for x in line.split()[-4:]]
3188 dx, dy = r-l, t-b
3189 break
3191 return dx, dy
3194class TableLiner(object):
3195 '''
3196 Utility class to turn tables into lines.
3197 '''
3199 def __init__(self, in_columns=None, in_rows=None, encoding='utf-8'):
3200 self.in_columns = in_columns
3201 self.in_rows = in_rows
3202 self.encoding = encoding
3204 def __iter__(self):
3205 if self.in_columns is not None:
3206 for row in zip(*self.in_columns):
3207 yield (' '.join([newstr(x) for x in row])+'\n').encode(
3208 self.encoding)
3210 if self.in_rows is not None:
3211 for row in self.in_rows:
3212 yield (' '.join([newstr(x) for x in row])+'\n').encode(
3213 self.encoding)
3216class LineStreamChopper(object):
3217 '''
3218 File-like object to buffer data.
3219 '''
3221 def __init__(self, liner):
3222 self.chopsize = None
3223 self.liner = liner
3224 self.chop_iterator = None
3225 self.closed = False
3227 def _chopiter(self):
3228 buf = BytesIO()
3229 for line in self.liner:
3230 buf.write(line)
3231 buflen = buf.tell()
3232 if self.chopsize is not None and buflen >= self.chopsize:
3233 buf.seek(0)
3234 while buf.tell() <= buflen-self.chopsize:
3235 yield buf.read(self.chopsize)
3237 newbuf = BytesIO()
3238 newbuf.write(buf.read())
3239 buf.close()
3240 buf = newbuf
3242 yield(buf.getvalue())
3243 buf.close()
3245 def read(self, size=None):
3246 if self.closed:
3247 raise ValueError('Cannot read from closed LineStreamChopper.')
3248 if self.chop_iterator is None:
3249 self.chopsize = size
3250 self.chop_iterator = self._chopiter()
3252 self.chopsize = size
3253 try:
3254 return next(self.chop_iterator)
3255 except StopIteration:
3256 return ''
3258 def close(self):
3259 self.chopsize = None
3260 self.chop_iterator = None
3261 self.closed = True
3263 def flush(self):
3264 pass
3267font_tab = {
3268 0: 'Helvetica',
3269 1: 'Helvetica-Bold',
3270}
3272font_tab_rev = dict((v, k) for (k, v) in font_tab.items())
3275class GMT(object):
3276 '''
3277 A thin wrapper to GMT command execution.
3279 A dict ``config`` may be given to override some of the default GMT
3280 parameters. The ``version`` argument may be used to select a specific GMT
3281 version, which should be used with this GMT instance. The selected
3282 version of GMT has to be installed on the system, must be supported by
3283 gmtpy and gmtpy must know where to find it.
3285 Each instance of this class is used for the task of producing one PS or PDF
3286 output file.
3288 Output of a series of GMT commands is accumulated in memory and can then be
3289 saved as PS or PDF file using the :py:meth:`save` method.
3291 GMT commands are accessed as method calls to instances of this class. See
3292 the :py:meth:`__getattr__` method for details on how the method's
3293 arguments are translated into options and arguments for the GMT command.
3295 Associated with each instance of this class, a temporary directory is
3296 created, where temporary files may be created, and which is automatically
3297 deleted, when the object is destroyed. The :py:meth:`tempfilename` method
3298 may be used to get a random filename in the instance's temporary directory.
3300 Any .gmtdefaults files are ignored. The GMT class uses a fixed
3301 set of defaults, which may be altered via an argument to the constructor.
3302 If possible, GMT is run in 'isolation mode', which was introduced with GMT
3303 version 4.2.2, by setting `GMT_TMPDIR` to the instance's temporary
3304 directory. With earlier versions of GMT, problems may arise with parallel
3305 execution of more than one GMT instance.
3307 Each instance of the GMT class may pick a specific version of GMT which
3308 shall be used, so that, if multiple versions of GMT are installed on the
3309 system, different versions of GMT can be used simultaneously such that
3310 backward compatibility of the scripts can be maintained.
3312 '''
3314 def __init__(
3315 self,
3316 config=None,
3317 kontinue=None,
3318 version='newest',
3319 config_papersize=None,
3320 eps_mode=False):
3322 self.installation = get_gmt_installation(version)
3323 self.gmt_config = dict(self.installation['defaults'])
3324 self.eps_mode = eps_mode
3325 self._shutil = shutil
3327 if config:
3328 self.gmt_config.update(config)
3330 if config_papersize:
3331 if not isinstance(config_papersize, str):
3332 config_papersize = 'Custom_%ix%i' % (
3333 int(config_papersize[0]), int(config_papersize[1]))
3335 if self.is_gmt5():
3336 self.gmt_config['PS_MEDIA'] = config_papersize
3337 else:
3338 self.gmt_config['PAPER_MEDIA'] = config_papersize
3340 self.tempdir = tempfile.mkdtemp("", "gmtpy-")
3341 self.gmt_config_filename = pjoin(self.tempdir, 'gmt.conf')
3342 self.gen_gmt_config_file(self.gmt_config_filename, self.gmt_config)
3344 if kontinue is not None:
3345 self.load_unfinished(kontinue)
3346 self.needstart = False
3347 else:
3348 self.output = BytesIO()
3349 self.needstart = True
3351 self.finished = False
3353 self.environ = os.environ.copy()
3354 self.environ['GMTHOME'] = self.installation.get('home', '')
3355 # GMT isolation mode: works only properly with GMT version >= 4.2.2
3356 self.environ['GMT_TMPDIR'] = self.tempdir
3358 self.layout = None
3359 self.command_log = []
3360 self.keep_temp_dir = False
3362 def is_gmt5(self):
3363 return self.installation['version'][0] == '5'
3365 def get_version(self):
3366 return self.installation['version']
3368 def get_config(self, key):
3369 return self.gmt_config[key]
3371 def to_points(self, string):
3372 if not string:
3373 return 0
3375 unit = string[-1]
3376 if unit in _units:
3377 return float(string[:-1])/_units[unit]
3378 else:
3379 default_unit = measure_unit(self.gmt_config).lower()[0]
3380 return float(string)/_units[default_unit]
3382 def label_font_size(self):
3383 if self.is_gmt5():
3384 return self.to_points(self.gmt_config['FONT_LABEL'].split(',')[0])
3385 else:
3386 return self.to_points(self.gmt_config['LABEL_FONT_SIZE'])
3388 def label_font(self):
3389 if self.is_gmt5():
3390 return font_tab_rev(self.gmt_config['FONT_LABEL'].split(',')[1])
3391 else:
3392 return self.gmt_config['LABEL_FONT']
3394 def gen_gmt_config_file(self, config_filename, config):
3395 f = open(config_filename, 'wb')
3396 f.write(
3397 ('#\n# GMT %s Defaults file\n'
3398 % self.installation['version']).encode('ascii'))
3400 for k, v in config.items():
3401 f.write(('%s = %s\n' % (k, v)).encode('ascii'))
3402 f.close()
3404 def __del__(self):
3405 if not self.keep_temp_dir:
3406 self._shutil.rmtree(self.tempdir)
3408 def _gmtcommand(self, command, *addargs, **kwargs):
3410 '''
3411 Execute arbitrary GMT command.
3413 See docstring in __getattr__ for details.
3414 '''
3416 in_stream = kwargs.pop('in_stream', None)
3417 in_filename = kwargs.pop('in_filename', None)
3418 in_string = kwargs.pop('in_string', None)
3419 in_columns = kwargs.pop('in_columns', None)
3420 in_rows = kwargs.pop('in_rows', None)
3421 out_stream = kwargs.pop('out_stream', None)
3422 out_filename = kwargs.pop('out_filename', None)
3423 out_discard = kwargs.pop('out_discard', None)
3424 finish = kwargs.pop('finish', False)
3425 suppressdefaults = kwargs.pop('suppress_defaults', False)
3426 config_override = kwargs.pop('config', None)
3428 assert(not self.finished)
3430 # check for mutual exclusiveness on input and output possibilities
3431 assert(1 >= len(
3432 [x for x in [
3433 in_stream, in_filename, in_string, in_columns, in_rows]
3434 if x is not None]))
3435 assert(1 >= len([x for x in [out_stream, out_filename, out_discard]
3436 if x is not None]))
3438 options = []
3440 gmt_config = self.gmt_config
3441 if not self.is_gmt5():
3442 gmt_config_filename = self.gmt_config_filename
3443 if config_override:
3444 gmt_config = self.gmt_config.copy()
3445 gmt_config.update(config_override)
3446 gmt_config_override_filename = pjoin(
3447 self.tempdir, 'gmtdefaults_override')
3448 self.gen_gmt_config_file(
3449 gmt_config_override_filename, gmt_config)
3450 gmt_config_filename = gmt_config_override_filename
3452 else: # gmt5 needs override variables as --VAR=value
3453 if config_override:
3454 for k, v in config_override.items():
3455 options.append('--%s=%s' % (k, v))
3457 if out_discard:
3458 out_filename = '/dev/null'
3460 out_mustclose = False
3461 if out_filename is not None:
3462 out_mustclose = True
3463 out_stream = open(out_filename, 'wb')
3465 if in_filename is not None:
3466 in_stream = open(in_filename, 'rb')
3468 if in_string is not None:
3469 in_stream = BytesIO(in_string)
3471 encoding_gmt = gmt_config.get(
3472 'PS_CHAR_ENCODING',
3473 gmt_config.get('CHAR_ENCODING', 'ISOLatin1+'))
3475 encoding = encoding_gmt_to_python[encoding_gmt.lower()]
3477 if in_columns is not None or in_rows is not None:
3478 in_stream = LineStreamChopper(TableLiner(in_columns=in_columns,
3479 in_rows=in_rows,
3480 encoding=encoding))
3482 # convert option arguments to strings
3483 for k, v in kwargs.items():
3484 if len(k) > 1:
3485 raise GmtPyError('Found illegal keyword argument "%s" '
3486 'while preparing options for command "%s"'
3487 % (k, command))
3489 if type(v) is bool:
3490 if v:
3491 options.append('-%s' % k)
3492 elif type(v) is tuple or type(v) is list:
3493 options.append('-%s' % k + '/'.join([str(x) for x in v]))
3494 else:
3495 options.append('-%s%s' % (k, str(v)))
3497 # if not redirecting to an external sink, handle -K -O
3498 if out_stream is None:
3499 if not finish:
3500 options.append('-K')
3501 else:
3502 self.finished = True
3504 if not self.needstart:
3505 options.append('-O')
3506 else:
3507 self.needstart = False
3509 out_stream = self.output
3511 # run the command
3512 if self.is_gmt5():
3513 args = [pjoin(self.installation['bin'], 'gmt'), command]
3514 else:
3515 args = [pjoin(self.installation['bin'], command)]
3517 if not os.path.isfile(args[0]):
3518 raise OSError('No such file: %s' % args[0])
3519 args.extend(options)
3520 args.extend(addargs)
3521 if not self.is_gmt5() and not suppressdefaults:
3522 # does not seem to work with GMT 5 (and should not be necessary
3523 args.append('+'+gmt_config_filename)
3525 bs = 2048
3526 p = subprocess.Popen(args, stdin=subprocess.PIPE,
3527 stdout=subprocess.PIPE, bufsize=bs,
3528 env=self.environ)
3529 while True:
3530 cr, cw, cx = select([p.stdout], [p.stdin], [])
3531 if cr:
3532 out_stream.write(p.stdout.read(bs))
3533 if cw:
3534 if in_stream is not None:
3535 data = in_stream.read(bs)
3536 if len(data) == 0:
3537 break
3538 p.stdin.write(data)
3539 else:
3540 break
3541 if not cr and not cw:
3542 break
3544 p.stdin.close()
3546 while True:
3547 data = p.stdout.read(bs)
3548 if len(data) == 0:
3549 break
3550 out_stream.write(data)
3552 p.stdout.close()
3554 retcode = p.wait()
3556 if in_stream is not None:
3557 in_stream.close()
3559 if out_mustclose:
3560 out_stream.close()
3562 if retcode != 0:
3563 self.keep_temp_dir = True
3564 raise GMTError('Command %s returned an error. '
3565 'While executing command:\n%s'
3566 % (command, escape_shell_args(args)))
3568 self.command_log.append(args)
3570 def __getattr__(self, command):
3572 '''
3573 Maps to call self._gmtcommand(command, \\*addargs, \\*\\*kwargs).
3575 Execute arbitrary GMT command.
3577 Run a GMT command and by default append its postscript output to the
3578 output file maintained by the GMT instance on which this method is
3579 called.
3581 Except for a few keyword arguments listed below, any ``kwargs`` and
3582 ``addargs`` are converted into command line options and arguments and
3583 passed to the GMT command. Numbers in keyword arguments are converted
3584 into strings. E.g. ``S=10`` is translated into ``'-S10'``. Tuples of
3585 numbers or strings are converted into strings where the elements of the
3586 tuples are separated by slashes '/'. E.g. ``R=(10, 10, 20, 20)`` is
3587 translated into ``'-R10/10/20/20'``. Options with a boolean argument
3588 are only appended to the GMT command, if their values are True.
3590 If no output redirection is in effect, the -K and -O options are
3591 handled by gmtpy and thus should not be specified. Use
3592 ``out_discard=True`` if you don't want -K or -O beeing added, but are
3593 not interested in the output.
3595 The standard input of the GMT process is fed by data selected with one
3596 of the following ``in_*`` keyword arguments:
3598 =============== =======================================================
3599 ``in_stream`` Data is read from an open file like object.
3600 ``in_filename`` Data is read from the given file.
3601 ``in_string`` String content is dumped to the process.
3602 ``in_columns`` A 2D nested iterable whose elements can be accessed as
3603 ``in_columns[icolumn][irow]`` is converted into an
3604 ascii
3605 table, which is fed to the process.
3606 ``in_rows`` A 2D nested iterable whos elements can be accessed as
3607 ``in_rows[irow][icolumn]`` is converted into an ascii
3608 table, which is fed to the process.
3609 =============== =======================================================
3611 The standard output of the GMT process may be redirected by one of the
3612 following options:
3614 ================= =====================================================
3615 ``out_stream`` Output is fed to an open file like object.
3616 ``out_filename`` Output is dumped to the given file.
3617 ``out_discard`` If True, output is dumped to :file:`/dev/null`.
3618 ================= =====================================================
3620 Additional keyword arguments:
3622 ===================== =================================================
3623 ``config`` Dict with GMT defaults which override the
3624 currently active set of defaults exclusively
3625 during this call.
3626 ``finish`` If True, the postscript file, which is maintained
3627 by the GMT instance is finished, and no further
3628 plotting is allowed.
3629 ``suppress_defaults`` Suppress appending of the ``'+gmtdefaults'``
3630 option to the command.
3631 ===================== =================================================
3633 '''
3635 def f(*args, **kwargs):
3636 return self._gmtcommand(command, *args, **kwargs)
3637 return f
3639 def tempfilename(self, name=None):
3640 '''
3641 Get filename for temporary file in the private temp directory.
3643 If no ``name`` argument is given, a random name is picked. If
3644 ``name`` is given, returns a path ending in that ``name``.
3645 '''
3647 if not name:
3648 name = ''.join(
3649 [random.choice('abcdefghijklmnopqrstuvwxyz')
3650 for i in range(10)])
3652 fn = pjoin(self.tempdir, name)
3653 return fn
3655 def tempfile(self, name=None):
3656 '''
3657 Create and open a file in the private temp directory.
3658 '''
3660 fn = self.tempfilename(name)
3661 f = open(fn, 'wb')
3662 return f, fn
3664 def save_unfinished(self, filename):
3665 out = open(filename, 'wb')
3666 out.write(self.output.getvalue())
3667 out.close()
3669 def load_unfinished(self, filename):
3670 self.output = BytesIO()
3671 self.finished = False
3672 inp = open(filename, 'rb')
3673 self.output.write(inp.read())
3674 inp.close()
3676 def dump(self, ident):
3677 filename = self.tempfilename('breakpoint-%s' % ident)
3678 self.save_unfinished(filename)
3680 def load(self, ident):
3681 filename = self.tempfilename('breakpoint-%s' % ident)
3682 self.load_unfinished(filename)
3684 def save(self, filename=None, bbox=None, resolution=150, oversample=2.,
3685 width=None, height=None, size=None, crop_eps_mode=False,
3686 psconvert=False):
3688 '''
3689 Finish and save figure as PDF, PS or PPM file.
3691 If filename ends with ``'.pdf'`` a PDF file is created by piping the
3692 GMT output through :program:`gmtpy-epstopdf`.
3694 If filename ends with ``'.png'`` a PNG file is created by running
3695 :program:`gmtpy-epstopdf`, :program:`pdftocairo` and
3696 :program:`convert`. ``resolution`` specifies the resolution in DPI for
3697 raster file formats. Rasterization is done at a higher resolution if
3698 ``oversample`` is set to a value higher than one. The output image size
3699 can also be controlled by setting ``width``, ``height`` or ``size``
3700 instead of ``resolution``. When ``size`` is given, the image is scaled
3701 so that ``max(width, height) == size``.
3703 The bounding box is set according to the values given in ``bbox``.
3704 '''
3706 if not self.finished:
3707 self.psxy(R=True, J=True, finish=True)
3709 if filename:
3710 tempfn = pjoin(self.tempdir, 'incomplete')
3711 out = open(tempfn, 'wb')
3712 else:
3713 out = sys.stdout
3715 if bbox and not self.is_gmt5():
3716 out.write(replace_bbox(bbox, self.output.getvalue()))
3717 else:
3718 out.write(self.output.getvalue())
3720 if filename:
3721 out.close()
3723 if filename.endswith('.ps') or (
3724 not self.is_gmt5() and filename.endswith('.eps')):
3726 shutil.move(tempfn, filename)
3727 return
3729 if self.is_gmt5():
3730 if crop_eps_mode:
3731 addarg = ['-A']
3732 else:
3733 addarg = []
3735 subprocess.call(
3736 [pjoin(self.installation['bin'], 'gmt'), 'psconvert',
3737 '-Te', '-F%s' % tempfn + '.eps', tempfn, ] + addarg)
3739 if bbox:
3740 with open(tempfn + '.eps', 'rb') as fin:
3741 with open(tempfn + '-fixbb.eps', 'wb') as fout:
3742 replace_bbox(bbox, fin, fout)
3744 shutil.move(tempfn + '-fixbb.eps', tempfn + '.eps')
3746 else:
3747 shutil.move(tempfn, tempfn + '.eps')
3749 if filename.endswith('.eps'):
3750 shutil.move(tempfn + '.eps', filename)
3751 return
3753 elif filename.endswith('.pdf'):
3754 if psconvert:
3755 gmt_bin = pjoin(self.installation['bin'], 'gmt')
3756 subprocess.call([gmt_bin, 'psconvert', tempfn + '.eps', '-Tf',
3757 '-F' + filename])
3758 else:
3759 subprocess.call(['gmtpy-epstopdf', '--res=%i' % resolution,
3760 '--outfile=' + filename, tempfn + '.eps'])
3761 else:
3762 subprocess.call([
3763 'gmtpy-epstopdf',
3764 '--res=%i' % (resolution * oversample),
3765 '--outfile=' + tempfn + '.pdf', tempfn + '.eps'])
3767 convert_graph(
3768 tempfn + '.pdf', filename,
3769 resolution=resolution, oversample=oversample,
3770 size=size, width=width, height=height)
3772 def bbox(self):
3773 return get_bbox(self.output.getvalue())
3775 def get_command_log(self):
3776 '''
3777 Get the command log.
3778 '''
3780 return self.command_log
3782 def __str__(self):
3783 s = ''
3784 for com in self.command_log:
3785 s += com[0] + "\n " + "\n ".join(com[1:]) + "\n\n"
3786 return s
3788 def page_size_points(self):
3789 '''
3790 Try to get paper size of output postscript file in points.
3791 '''
3793 pm = paper_media(self.gmt_config).lower()
3794 if pm.endswith('+') or pm.endswith('-'):
3795 pm = pm[:-1]
3797 orient = page_orientation(self.gmt_config).lower()
3799 if pm in all_paper_sizes():
3801 if orient == 'portrait':
3802 return get_paper_size(pm)
3803 else:
3804 return get_paper_size(pm)[1], get_paper_size(pm)[0]
3806 m = re.match(r'custom_([0-9.]+)([cimp]?)x([0-9.]+)([cimp]?)', pm)
3807 if m:
3808 w, uw, h, uh = m.groups()
3809 w, h = float(w), float(h)
3810 if uw:
3811 w *= _units[uw]
3812 if uh:
3813 h *= _units[uh]
3814 if orient == 'portrait':
3815 return w, h
3816 else:
3817 return h, w
3819 return None, None
3821 def default_layout(self, with_palette=False):
3822 '''
3823 Get a default layout for the output page.
3825 One of three different layouts is choosen, depending on the
3826 `PAPER_MEDIA` setting in the GMT configuration dict.
3828 If `PAPER_MEDIA` ends with a ``'+'`` (EPS output is selected), a
3829 :py:class:`FrameLayout` is centered on the page, whose size is
3830 controlled by its center widget's size plus the margins of the
3831 :py:class:`FrameLayout`.
3833 If `PAPER_MEDIA` indicates, that a custom page size is wanted by
3834 starting with ``'Custom_'``, a :py:class:`FrameLayout` is used to fill
3835 the complete page. The center widget's size is then controlled by the
3836 page's size minus the margins of the :py:class:`FrameLayout`.
3838 In any other case, two FrameLayouts are nested, such that the outer
3839 layout attaches a 1 cm (printer) margin around the complete page, and
3840 the inner FrameLayout's center widget takes up as much space as
3841 possible under the constraint, that an aspect ratio of 1/golden_ratio
3842 is preserved.
3844 In any case, a reference to the innermost :py:class:`FrameLayout`
3845 instance is returned. The top-level layout can be accessed by calling
3846 :py:meth:`Widget.get_parent` on the returned layout.
3847 '''
3849 if self.layout is None:
3850 w, h = self.page_size_points()
3852 if w is None or h is None:
3853 raise GmtPyError("Can't determine page size for layout")
3855 pm = paper_media(self.gmt_config).lower()
3857 if with_palette:
3858 palette_layout = GridLayout(3, 1)
3859 spacer = palette_layout.get_widget(1, 0)
3860 palette_widget = palette_layout.get_widget(2, 0)
3861 spacer.set_horizontal(0.5*cm)
3862 palette_widget.set_horizontal(0.5*cm)
3864 if pm.endswith('+') or self.eps_mode:
3865 outer = CenterLayout()
3866 outer.set_policy((w, h), (0., 0.))
3867 inner = FrameLayout()
3868 outer.set_widget(inner)
3869 if with_palette:
3870 inner.set_widget('center', palette_layout)
3871 widget = palette_layout
3872 else:
3873 widget = inner.get_widget('center')
3874 widget.set_policy((w/golden_ratio, 0.), (0., 0.),
3875 aspect=1./golden_ratio)
3876 mw = 3.0*cm
3877 inner.set_fixed_margins(
3878 mw, mw, mw/golden_ratio, mw/golden_ratio)
3879 self.layout = inner
3881 elif pm.startswith('custom_'):
3882 layout = FrameLayout()
3883 layout.set_policy((w, h), (0., 0.))
3884 mw = 3.0*cm
3885 layout.set_min_margins(
3886 mw, mw, mw/golden_ratio, mw/golden_ratio)
3887 if with_palette:
3888 layout.set_widget('center', palette_layout)
3889 self.layout = layout
3890 else:
3891 outer = FrameLayout()
3892 outer.set_policy((w, h), (0., 0.))
3893 outer.set_fixed_margins(1.*cm, 1.*cm, 1.*cm, 1.*cm)
3895 inner = FrameLayout()
3896 outer.set_widget('center', inner)
3897 mw = 3.0*cm
3898 inner.set_min_margins(mw, mw, mw/golden_ratio, mw/golden_ratio)
3899 if with_palette:
3900 inner.set_widget('center', palette_layout)
3901 widget = palette_layout
3902 else:
3903 widget = inner.get_widget('center')
3905 widget.set_aspect(1./golden_ratio)
3907 self.layout = inner
3909 return self.layout
3911 def draw_layout(self, layout):
3912 '''
3913 Use psxy to draw layout; for debugging
3914 '''
3916 # corners = layout.get_corners(descend=True)
3917 rects = num.array(layout.get_sizes(), dtype=float)
3918 rects_wid = rects[:, 0, 0]
3919 rects_hei = rects[:, 0, 1]
3920 rects_center_x = rects[:, 1, 0] + rects_wid*0.5
3921 rects_center_y = rects[:, 1, 1] + rects_hei*0.5
3922 nrects = len(rects)
3923 prects = (rects_center_x, rects_center_y, num.arange(nrects),
3924 num.zeros(nrects), rects_hei, rects_wid)
3926 # points = num.array(corners, dtype=float)
3928 cptfile = self.tempfilename() + '.cpt'
3929 self.makecpt(
3930 C='ocean',
3931 T='%g/%g/%g' % (-nrects, nrects, 1),
3932 Z=True,
3933 out_filename=cptfile, suppress_defaults=True)
3935 bb = layout.bbox()
3936 self.psxy(
3937 in_columns=prects,
3938 C=cptfile,
3939 W='1p',
3940 S='J',
3941 R=(bb[0], bb[2], bb[1], bb[3]),
3942 *layout.XYJ())
3945def simpleconf_to_ax(conf, axname):
3946 c = {}
3947 x = axname
3948 for x in ('', axname):
3949 for k in ('label', 'unit', 'scaled_unit', 'scaled_unit_factor',
3950 'space', 'mode', 'approx_ticks', 'limits', 'masking', 'inc',
3951 'snap'):
3953 if x+k in conf:
3954 c[k] = conf[x+k]
3956 return Ax(**c)
3959class DensityPlotDef(object):
3960 def __init__(self, data, cpt='ocean', tension=0.7, size=(640, 480),
3961 contour=False, method='surface', zscaler=None, **extra):
3962 self.data = data
3963 self.cpt = cpt
3964 self.tension = tension
3965 self.size = size
3966 self.contour = contour
3967 self.method = method
3968 self.zscaler = zscaler
3969 self.extra = extra
3972class TextDef(object):
3973 def __init__(
3974 self,
3975 data,
3976 size=9,
3977 justify='MC',
3978 fontno=0,
3979 offset=(0, 0),
3980 color='black'):
3982 self.data = data
3983 self.size = size
3984 self.justify = justify
3985 self.fontno = fontno
3986 self.offset = offset
3987 self.color = color
3990class Simple(object):
3991 def __init__(self, gmtconfig=None, gmtversion='newest', **simple_config):
3992 self.data = []
3993 self.symbols = []
3994 self.config = copy.deepcopy(simple_config)
3995 self.gmtconfig = gmtconfig
3996 self.density_plot_defs = []
3997 self.text_defs = []
3999 self.gmtversion = gmtversion
4001 self.data_x = []
4002 self.symbols_x = []
4004 self.data_y = []
4005 self.symbols_y = []
4007 self.default_config = {}
4008 self.set_defaults(width=15.*cm,
4009 height=15.*cm / golden_ratio,
4010 margins=(2.*cm, 2.*cm, 2.*cm, 2.*cm),
4011 with_palette=False,
4012 palette_offset=0.5*cm,
4013 palette_width=None,
4014 palette_height=None,
4015 zlabeloffset=2*cm,
4016 draw_layout=False)
4018 self.setup_defaults()
4019 self.fixate_widget_aspect = False
4021 def setup_defaults(self):
4022 pass
4024 def set_defaults(self, **kwargs):
4025 self.default_config.update(kwargs)
4027 def plot(self, data, symbol=''):
4028 self.data.append(data)
4029 self.symbols.append(symbol)
4031 def density_plot(self, data, **kwargs):
4032 dpd = DensityPlotDef(data, **kwargs)
4033 self.density_plot_defs.append(dpd)
4035 def text(self, data, **kwargs):
4036 dpd = TextDef(data, **kwargs)
4037 self.text_defs.append(dpd)
4039 def plot_x(self, data, symbol=''):
4040 self.data_x.append(data)
4041 self.symbols_x.append(symbol)
4043 def plot_y(self, data, symbol=''):
4044 self.data_y.append(data)
4045 self.symbols_y.append(symbol)
4047 def set(self, **kwargs):
4048 self.config.update(kwargs)
4050 def setup_base(self, conf):
4051 w = conf.pop('width')
4052 h = conf.pop('height')
4053 margins = conf.pop('margins')
4055 gmtconfig = {}
4056 if self.gmtconfig is not None:
4057 gmtconfig.update(self.gmtconfig)
4059 gmt = GMT(
4060 version=self.gmtversion,
4061 config=gmtconfig,
4062 config_papersize='Custom_%ix%i' % (w, h))
4064 layout = gmt.default_layout(with_palette=conf['with_palette'])
4065 layout.set_min_margins(*margins)
4066 if conf['with_palette']:
4067 widget = layout.get_widget().get_widget(0, 0)
4068 spacer = layout.get_widget().get_widget(1, 0)
4069 spacer.set_horizontal(conf['palette_offset'])
4070 palette_widget = layout.get_widget().get_widget(2, 0)
4071 if conf['palette_width'] is not None:
4072 palette_widget.set_horizontal(conf['palette_width'])
4073 if conf['palette_height'] is not None:
4074 palette_widget.set_vertical(conf['palette_height'])
4075 widget.set_vertical(h-margins[2]-margins[3]-0.03*cm)
4076 return gmt, layout, widget, palette_widget
4077 else:
4078 widget = layout.get_widget()
4079 return gmt, layout, widget, None
4081 def setup_projection(self, widget, scaler, conf):
4082 pass
4084 def setup_scaling(self, conf):
4085 ndims = 2
4086 if self.density_plot_defs:
4087 ndims = 3
4089 axes = [simpleconf_to_ax(conf, x) for x in 'xyz'[:ndims]]
4091 data_all = []
4092 data_all.extend(self.data)
4093 for dsd in self.density_plot_defs:
4094 if dsd.zscaler is None:
4095 data_all.append(dsd.data)
4096 else:
4097 data_all.append(dsd.data[:2])
4098 data_chopped = [ds[:ndims] for ds in data_all]
4100 scaler = ScaleGuru(data_chopped, axes=axes[:ndims])
4102 self.setup_scaling_plus(scaler, axes[:ndims])
4104 return scaler
4106 def setup_scaling_plus(self, scaler, axes):
4107 pass
4109 def setup_scaling_extra(self, scaler, conf):
4111 scaler_x = scaler.copy()
4112 scaler_x.data_ranges[1] = (0., 1.)
4113 scaler_x.axes[1].mode = 'off'
4115 scaler_y = scaler.copy()
4116 scaler_y.data_ranges[0] = (0., 1.)
4117 scaler_y.axes[0].mode = 'off'
4119 return scaler_x, scaler_y
4121 def draw_density(self, gmt, widget, scaler):
4123 R = scaler.R()
4124 # par = scaler.get_params()
4125 rxyj = R + widget.XYJ()
4126 innerticks = False
4127 for dpd in self.density_plot_defs:
4129 fn_cpt = gmt.tempfilename() + '.cpt'
4131 if dpd.zscaler is not None:
4132 s = dpd.zscaler
4133 else:
4134 s = scaler
4136 gmt.makecpt(C=dpd.cpt, out_filename=fn_cpt, *s.T())
4138 fn_grid = gmt.tempfilename()
4140 fn_mean = gmt.tempfilename()
4142 if dpd.method in ('surface', 'triangulate'):
4143 gmt.blockmean(in_columns=dpd.data,
4144 I='%i+/%i+' % dpd.size, # noqa
4145 out_filename=fn_mean, *R)
4147 if dpd.method == 'surface':
4148 gmt.surface(
4149 in_filename=fn_mean,
4150 T=dpd.tension,
4151 G=fn_grid,
4152 I='%i+/%i+' % dpd.size, # noqa
4153 out_discard=True,
4154 *R)
4156 if dpd.method == 'triangulate':
4157 gmt.triangulate(
4158 in_filename=fn_mean,
4159 G=fn_grid,
4160 I='%i+/%i+' % dpd.size, # noqa
4161 out_discard=True,
4162 V=True,
4163 *R)
4165 if gmt.is_gmt5():
4166 gmt.grdimage(fn_grid, C=fn_cpt, E='i', n='l', *rxyj)
4168 else:
4169 gmt.grdimage(fn_grid, C=fn_cpt, E='i', S='l', *rxyj)
4171 if dpd.contour:
4172 gmt.grdcontour(fn_grid, C=fn_cpt, W='0.5p,black', *rxyj)
4173 innerticks = '0.5p,black'
4175 os.remove(fn_grid)
4176 os.remove(fn_mean)
4178 if dpd.method == 'fillcontour':
4179 extra = dict(C=fn_cpt)
4180 extra.update(dpd.extra)
4181 gmt.pscontour(in_columns=dpd.data,
4182 I=True, *rxyj, **extra) # noqa
4184 if dpd.method == 'contour':
4185 extra = dict(W='0.5p,black', C=fn_cpt)
4186 extra.update(dpd.extra)
4187 gmt.pscontour(in_columns=dpd.data, *rxyj, **extra)
4189 return fn_cpt, innerticks
4191 def draw_basemap(self, gmt, widget, scaler):
4192 gmt.psbasemap(*(widget.JXY() + scaler.RB(ax_projection=True)))
4194 def draw(self, gmt, widget, scaler):
4195 rxyj = scaler.R() + widget.JXY()
4196 for dat, sym in zip(self.data, self.symbols):
4197 gmt.psxy(in_columns=dat, *(sym.split()+rxyj))
4199 def post_draw(self, gmt, widget, scaler):
4200 pass
4202 def pre_draw(self, gmt, widget, scaler):
4203 pass
4205 def draw_extra(self, gmt, widget, scaler_x, scaler_y):
4207 for dat, sym in zip(self.data_x, self.symbols_x):
4208 gmt.psxy(in_columns=dat,
4209 *(sym.split() + scaler_x.R() + widget.JXY()))
4211 for dat, sym in zip(self.data_y, self.symbols_y):
4212 gmt.psxy(in_columns=dat,
4213 *(sym.split() + scaler_y.R() + widget.JXY()))
4215 def draw_text(self, gmt, widget, scaler):
4217 rxyj = scaler.R() + widget.JXY()
4218 for td in self.text_defs:
4219 x, y = td.data[0:2]
4220 text = td.data[-1]
4221 size = td.size
4222 angle = 0
4223 fontno = td.fontno
4224 justify = td.justify
4225 color = td.color
4226 if gmt.is_gmt5():
4227 gmt.pstext(
4228 in_rows=[(x, y, text)],
4229 F='+f%gp,%s,%s+a%g+j%s' % (
4230 size, fontno, color, angle, justify),
4231 D='%gp/%gp' % td.offset, *rxyj)
4232 else:
4233 gmt.pstext(
4234 in_rows=[(x, y, size, angle, fontno, justify, text)],
4235 D='%gp/%gp' % td.offset, *rxyj)
4237 def save(self, filename, resolution=150):
4239 conf = dict(self.default_config)
4240 conf.update(self.config)
4242 gmt, layout, widget, palette_widget = self.setup_base(conf)
4243 scaler = self.setup_scaling(conf)
4244 scaler_x, scaler_y = self.setup_scaling_extra(scaler, conf)
4246 self.setup_projection(widget, scaler, conf)
4247 if self.fixate_widget_aspect:
4248 aspect = aspect_for_projection(
4249 gmt.installation['version'], *(widget.J() + scaler.R()))
4251 widget.set_aspect(aspect)
4253 if conf['draw_layout']:
4254 gmt.draw_layout(layout)
4255 cptfile = None
4256 if self.density_plot_defs:
4257 cptfile, innerticks = self.draw_density(gmt, widget, scaler)
4258 self.pre_draw(gmt, widget, scaler)
4259 self.draw(gmt, widget, scaler)
4260 self.post_draw(gmt, widget, scaler)
4261 self.draw_extra(gmt, widget, scaler_x, scaler_y)
4262 self.draw_text(gmt, widget, scaler)
4263 self.draw_basemap(gmt, widget, scaler)
4265 if palette_widget and cptfile:
4266 nice_palette(gmt, palette_widget, scaler, cptfile,
4267 innerticks=innerticks,
4268 zlabeloffset=conf['zlabeloffset'])
4270 gmt.save(filename, resolution=resolution)
4273class LinLinPlot(Simple):
4274 pass
4277class LogLinPlot(Simple):
4279 def setup_defaults(self):
4280 self.set_defaults(xmode='min-max')
4282 def setup_projection(self, widget, scaler, conf):
4283 widget['J'] = '-JX%(width)gpl/%(height)gp'
4284 scaler['B'] = '-B2:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen'
4287class LinLogPlot(Simple):
4289 def setup_defaults(self):
4290 self.set_defaults(ymode='min-max')
4292 def setup_projection(self, widget, scaler, conf):
4293 widget['J'] = '-JX%(width)gp/%(height)gpl'
4294 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/2:%(ylabel)s:WSen'
4297class LogLogPlot(Simple):
4299 def setup_defaults(self):
4300 self.set_defaults(mode='min-max')
4302 def setup_projection(self, widget, scaler, conf):
4303 widget['J'] = '-JX%(width)gpl/%(height)gpl'
4304 scaler['B'] = '-B2:%(xlabel)s:/2:%(ylabel)s:WSen'
4307class AziDistPlot(Simple):
4309 def __init__(self, *args, **kwargs):
4310 Simple.__init__(self, *args, **kwargs)
4311 self.fixate_widget_aspect = True
4313 def setup_defaults(self):
4314 self.set_defaults(
4315 height=15.*cm,
4316 width=15.*cm,
4317 xmode='off',
4318 xlimits=(0., 360.),
4319 xinc=45.)
4321 def setup_projection(self, widget, scaler, conf):
4322 widget['J'] = '-JPa%(width)gp'
4324 def setup_scaling_plus(self, scaler, axes):
4325 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/%(yinc)g:%(ylabel)s:N'
4328class MPlot(Simple):
4330 def __init__(self, *args, **kwargs):
4331 Simple.__init__(self, *args, **kwargs)
4332 self.fixate_widget_aspect = True
4334 def setup_defaults(self):
4335 self.set_defaults(xmode='min-max', ymode='min-max')
4337 def setup_projection(self, widget, scaler, conf):
4338 par = scaler.get_params()
4339 lon0 = (par['xmin'] + par['xmax'])/2.
4340 lat0 = (par['ymin'] + par['ymax'])/2.
4341 sll = '%g/%g' % (lon0, lat0)
4342 widget['J'] = '-JM' + sll + '/%(width)gp'
4343 scaler['B'] = \
4344 '-B%(xinc)gg%(xinc)g:%(xlabel)s:/%(yinc)gg%(yinc)g:%(ylabel)s:WSen'
4347def nice_palette(gmt, widget, scaleguru, cptfile, zlabeloffset=0.8*inch,
4348 innerticks=True):
4350 par = scaleguru.get_params()
4351 par_ax = scaleguru.get_params(ax_projection=True)
4352 nz_palette = int(widget.height()/inch * 300)
4353 px = num.zeros(nz_palette*2)
4354 px[1::2] += 1
4355 pz = num.linspace(par['zmin'], par['zmax'], nz_palette).repeat(2)
4356 pdz = pz[2]-pz[0]
4357 palgrdfile = gmt.tempfilename()
4358 pal_r = (0, 1, par['zmin'], par['zmax'])
4359 pal_ax_r = (0, 1, par_ax['zmin'], par_ax['zmax'])
4360 gmt.xyz2grd(
4361 G=palgrdfile, R=pal_r,
4362 I=(1, pdz), in_columns=(px, pz, pz), # noqa
4363 out_discard=True)
4365 gmt.grdimage(palgrdfile, R=pal_r, C=cptfile, *widget.JXY())
4366 if isinstance(innerticks, str):
4367 tickpen = innerticks
4368 gmt.grdcontour(palgrdfile, W=tickpen, R=pal_r, C=cptfile,
4369 *widget.JXY())
4371 negpalwid = '%gp' % -widget.width()
4372 if not isinstance(innerticks, str) and innerticks:
4373 ticklen = negpalwid
4374 else:
4375 ticklen = '0p'
4377 TICK_LENGTH_PARAM = 'MAP_TICK_LENGTH' if gmt.is_gmt5() else 'TICK_LENGTH'
4378 gmt.psbasemap(
4379 R=pal_ax_r, B='4::/%(zinc)g::nsw' % par_ax,
4380 config={TICK_LENGTH_PARAM: ticklen},
4381 *widget.JXY())
4383 if innerticks:
4384 gmt.psbasemap(
4385 R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax,
4386 config={TICK_LENGTH_PARAM: '0p'},
4387 *widget.JXY())
4388 else:
4389 gmt.psbasemap(R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax, *widget.JXY())
4391 if par_ax['zlabel']:
4392 label_font = gmt.label_font()
4393 label_font_size = gmt.label_font_size()
4394 label_offset = zlabeloffset
4395 gmt.pstext(
4396 R=(0, 1, 0, 2), D="%gp/0p" % label_offset,
4397 N=True,
4398 in_rows=[(1, 1, label_font_size, -90, label_font, 'CB',
4399 par_ax['zlabel'])],
4400 *widget.JXY())