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 if version[0] == '6':
1301 raise GMTInstallationProblem(
1302 'pyrocko.gmtpy does not support GMT 6')
1304 if version[0] != '5':
1305 gmtdefaults = pjoin(bin_dir, 'gmtdefaults')
1307 versionfound = get_gmt_version(gmtdefaults, home_dir)
1309 if versionfound != version:
1310 raise GMTInstallationProblem((
1311 'Expected GMT version %s but found version %s.\n'
1312 '(Looking at output of %s)') % (
1313 version, versionfound, gmtdefaults))
1316def get_gmt_installation(version):
1317 setup_gmt_installations()
1318 if version != 'newest' and version not in _gmt_installations:
1319 logging.warn('GMT version %s not installed, taking version %s instead'
1320 % (version, newest_installed_gmt_version()))
1322 version = 'newest'
1324 if version == 'newest':
1325 version = newest_installed_gmt_version()
1327 installation = dict(_gmt_installations[version])
1329 return installation
1332def setup_gmt_installations():
1333 if not setup_gmt_installations.have_done:
1334 if not _gmt_installations:
1336 _gmt_installations.update(detect_gmt_installations())
1338 # store defaults as dicts into the gmt installations dicts
1339 for version, installation in _gmt_installations.items():
1340 installation['defaults'] = gmt_default_config(version)
1341 installation['version'] = version
1343 for installation in _gmt_installations.values():
1344 check_gmt_installation(installation)
1346 setup_gmt_installations.have_done = True
1349setup_gmt_installations.have_done = False
1351_paper_sizes_a = '''A0 2380 3368
1352 A1 1684 2380
1353 A2 1190 1684
1354 A3 842 1190
1355 A4 595 842
1356 A5 421 595
1357 A6 297 421
1358 A7 210 297
1359 A8 148 210
1360 A9 105 148
1361 A10 74 105
1362 B0 2836 4008
1363 B1 2004 2836
1364 B2 1418 2004
1365 B3 1002 1418
1366 B4 709 1002
1367 B5 501 709
1368 archA 648 864
1369 archB 864 1296
1370 archC 1296 1728
1371 archD 1728 2592
1372 archE 2592 3456
1373 flsa 612 936
1374 halfletter 396 612
1375 note 540 720
1376 letter 612 792
1377 legal 612 1008
1378 11x17 792 1224
1379 ledger 1224 792'''
1382_paper_sizes = {}
1385def setup_paper_sizes():
1386 if not _paper_sizes:
1387 for line in _paper_sizes_a.splitlines():
1388 k, w, h = line.split()
1389 _paper_sizes[k.lower()] = float(w), float(h)
1392def get_paper_size(k):
1393 setup_paper_sizes()
1394 return _paper_sizes[k.lower().rstrip('+')]
1397def all_paper_sizes():
1398 setup_paper_sizes()
1399 return _paper_sizes
1402def measure_unit(gmt_config):
1403 for k in ['MEASURE_UNIT', 'PROJ_LENGTH_UNIT']:
1404 if k in gmt_config:
1405 return gmt_config[k]
1407 raise GmtPyError('cannot get measure unit / proj length unit from config')
1410def paper_media(gmt_config):
1411 for k in ['PAPER_MEDIA', 'PS_MEDIA']:
1412 if k in gmt_config:
1413 return gmt_config[k]
1415 raise GmtPyError('cannot get paper media from config')
1418def page_orientation(gmt_config):
1419 for k in ['PAGE_ORIENTATION', 'PS_PAGE_ORIENTATION']:
1420 if k in gmt_config:
1421 return gmt_config[k]
1423 raise GmtPyError('cannot get paper orientation from config')
1426def make_bbox(width, height, gmt_config, margins=(0.8, 0.8, 0.8, 0.8)):
1428 leftmargin, topmargin, rightmargin, bottommargin = margins
1429 portrait = page_orientation(gmt_config).lower() == 'portrait'
1431 paper_size = get_paper_size(paper_media(gmt_config))
1432 if not portrait:
1433 paper_size = paper_size[1], paper_size[0]
1435 xoffset = (paper_size[0] - (width + leftmargin + rightmargin)) / \
1436 2.0 + leftmargin
1437 yoffset = (paper_size[1] - (height + topmargin + bottommargin)) / \
1438 2.0 + bottommargin
1440 if portrait:
1441 bb1 = int((xoffset - leftmargin))
1442 bb2 = int((yoffset - bottommargin))
1443 bb3 = bb1 + int((width+leftmargin+rightmargin))
1444 bb4 = bb2 + int((height+topmargin+bottommargin))
1445 else:
1446 bb1 = int((yoffset - topmargin))
1447 bb2 = int((xoffset - leftmargin))
1448 bb3 = bb1 + int((height+topmargin+bottommargin))
1449 bb4 = bb2 + int((width+leftmargin+rightmargin))
1451 return xoffset, yoffset, (bb1, bb2, bb3, bb4)
1454def gmtdefaults_as_text(version='newest'):
1456 '''
1457 Get the built-in gmtdefaults.
1458 '''
1460 if version not in _gmt_installations:
1461 logging.warn('GMT version %s not installed, taking version %s instead'
1462 % (version, newest_installed_gmt_version()))
1463 version = 'newest'
1465 if version == 'newest':
1466 version = newest_installed_gmt_version()
1468 return _gmt_defaults_by_version[version]
1471def savegrd(x, y, z, filename, title=None, naming='xy'):
1472 '''
1473 Write COARDS compliant netcdf (grd) file.
1474 '''
1476 assert y.size, x.size == z.shape
1477 ny, nx = z.shape
1478 nc = netcdf.netcdf_file(filename, 'w')
1479 assert naming in ('xy', 'lonlat')
1481 if naming == 'xy':
1482 kx, ky = 'x', 'y'
1483 else:
1484 kx, ky = 'lon', 'lat'
1486 nc.node_offset = 0
1487 if title is not None:
1488 nc.title = title
1490 nc.Conventions = 'COARDS/CF-1.0'
1491 nc.createDimension(kx, nx)
1492 nc.createDimension(ky, ny)
1494 xvar = nc.createVariable(kx, 'd', (kx,))
1495 yvar = nc.createVariable(ky, 'd', (ky,))
1496 if naming == 'xy':
1497 xvar.long_name = kx
1498 yvar.long_name = ky
1499 else:
1500 xvar.long_name = 'longitude'
1501 xvar.units = 'degrees_east'
1502 yvar.long_name = 'latitude'
1503 yvar.units = 'degrees_north'
1505 zvar = nc.createVariable('z', 'd', (ky, kx))
1507 xvar[:] = x.astype(num.float64)
1508 yvar[:] = y.astype(num.float64)
1509 zvar[:] = z.astype(num.float64)
1511 nc.close()
1514def to_array(var):
1515 arr = var[:].copy()
1516 if hasattr(var, 'scale_factor'):
1517 arr *= var.scale_factor
1519 if hasattr(var, 'add_offset'):
1520 arr += var.add_offset
1522 return arr
1525def loadgrd(filename):
1526 '''
1527 Read COARDS compliant netcdf (grd) file.
1528 '''
1530 nc = netcdf.netcdf_file(filename, 'r')
1531 vkeys = list(nc.variables.keys())
1532 kx = 'x'
1533 ky = 'y'
1534 if 'lon' in vkeys:
1535 kx = 'lon'
1536 if 'lat' in vkeys:
1537 ky = 'lat'
1539 x = to_array(nc.variables[kx])
1540 y = to_array(nc.variables[ky])
1541 z = to_array(nc.variables['z'])
1543 nc.close()
1544 return x, y, z
1547def centers_to_edges(asorted):
1548 return (asorted[1:] + asorted[:-1])/2.
1551def nvals(asorted):
1552 eps = (asorted[-1]-asorted[0])/asorted.size
1553 return num.sum(asorted[1:] - asorted[:-1] >= eps) + 1
1556def guess_vals(asorted):
1557 eps = (asorted[-1]-asorted[0])/asorted.size
1558 indis = num.nonzero(asorted[1:] - asorted[:-1] >= eps)[0]
1559 indis = num.concatenate((num.array([0]), indis+1,
1560 num.array([asorted.size])))
1561 asum = num.zeros(asorted.size+1)
1562 asum[1:] = num.cumsum(asorted)
1563 return (asum[indis[1:]] - asum[indis[:-1]]) / (indis[1:]-indis[:-1])
1566def blockmean(asorted, b):
1567 indis = num.nonzero(asorted[1:] - asorted[:-1])[0]
1568 indis = num.concatenate((num.array([0]), indis+1,
1569 num.array([asorted.size])))
1570 bsum = num.zeros(b.size+1)
1571 bsum[1:] = num.cumsum(b)
1572 return (
1573 asorted[indis[:-1]],
1574 (bsum[indis[1:]] - bsum[indis[:-1]]) / (indis[1:]-indis[:-1]))
1577def griddata_regular(x, y, z, xvals, yvals):
1578 nx, ny = xvals.size, yvals.size
1579 xindi = num.digitize(x, centers_to_edges(xvals))
1580 yindi = num.digitize(y, centers_to_edges(yvals))
1582 zindi = yindi*nx+xindi
1583 order = num.argsort(zindi)
1584 z = z[order]
1585 zindi = zindi[order]
1587 zindi, z = blockmean(zindi, z)
1588 znew = num.empty(nx*ny, dtype=float)
1589 znew[:] = num.nan
1590 znew[zindi] = z
1591 return znew.reshape(ny, nx)
1594def guess_field_size(x_sorted, y_sorted, z=None, mode=None):
1595 critical_fraction = 1./num.e - 0.014*3
1596 xs = x_sorted
1597 ys = y_sorted
1598 nxs, nys = nvals(xs), nvals(ys)
1599 if mode == 'nonrandom':
1600 return nxs, nys, 0
1601 elif xs.size == nxs*nys:
1602 # exact match
1603 return nxs, nys, 0
1604 elif nxs >= xs.size*critical_fraction and nys >= xs.size*critical_fraction:
1605 # possibly randomly sampled
1606 nxs = int(math.sqrt(xs.size))
1607 nys = nxs
1608 return nxs, nys, 2
1609 else:
1610 return nxs, nys, 1
1613def griddata_auto(x, y, z, mode=None):
1614 '''
1615 Grid tabular XYZ data by binning.
1617 This function does some extra work to guess the size of the grid. This
1618 should work fine if the input values are already defined on an rectilinear
1619 grid, even if data points are missing or duplicated. This routine also
1620 tries to detect a random distribution of input data and in that case
1621 creates a grid of size sqrt(N) x sqrt(N).
1623 The points do not have to be given in any particular order. Grid nodes
1624 without data are assigned the NaN value. If multiple data points map to the
1625 same grid node, their average is assigned to the grid node.
1626 '''
1628 x, y, z = [num.asarray(X) for X in (x, y, z)]
1629 assert x.size == y.size == z.size
1630 xs, ys = num.sort(x), num.sort(y)
1631 nx, ny, badness = guess_field_size(xs, ys, z, mode=mode)
1632 if badness <= 1:
1633 xf = guess_vals(xs)
1634 yf = guess_vals(ys)
1635 zf = griddata_regular(x, y, z, xf, yf)
1636 else:
1637 xf = num.linspace(xs[0], xs[-1], nx)
1638 yf = num.linspace(ys[0], ys[-1], ny)
1639 zf = griddata_regular(x, y, z, xf, yf)
1641 return xf, yf, zf
1644def tabledata(xf, yf, zf):
1645 assert yf.size, xf.size == zf.shape
1646 x = num.tile(xf, yf.size)
1647 y = num.repeat(yf, xf.size)
1648 z = zf.flatten()
1649 return x, y, z
1652def double1d(a):
1653 a2 = num.empty(a.size*2-1)
1654 a2[::2] = a
1655 a2[1::2] = (a[:-1] + a[1:])/2.
1656 return a2
1659def double2d(f):
1660 f2 = num.empty((f.shape[0]*2-1, f.shape[1]*2-1))
1661 f2[:, :] = num.nan
1662 f2[::2, ::2] = f
1663 f2[1::2, ::2] = (f[:-1, :] + f[1:, :])/2.
1664 f2[::2, 1::2] = (f[:, :-1] + f[:, 1:])/2.
1665 f2[1::2, 1::2] = (f[:-1, :-1] + f[1:, :-1] + f[:-1, 1:] + f[1:, 1:])/4.
1666 diag = f2[1::2, 1::2]
1667 diagA = (f[:-1, :-1] + f[1:, 1:]) / 2.
1668 diagB = (f[1:, :-1] + f[:-1, 1:]) / 2.
1669 f2[1::2, 1::2] = num.where(num.isnan(diag), diagA, diag)
1670 f2[1::2, 1::2] = num.where(num.isnan(diag), diagB, diag)
1671 return f2
1674def doublegrid(x, y, z):
1675 x2 = double1d(x)
1676 y2 = double1d(y)
1677 z2 = double2d(z)
1678 return x2, y2, z2
1681class Guru(object):
1682 '''
1683 Abstract base class providing template interpolation, accessible as
1684 attributes.
1686 Classes deriving from this one, have to implement a :py:meth:`get_params`
1687 method, which is called to get a dict to do ordinary
1688 ``"%(key)x"``-substitutions. The deriving class must also provide a dict
1689 with the templates.
1690 '''
1692 def __init__(self):
1693 self.templates = {}
1695 def fill(self, templates, **kwargs):
1696 params = self.get_params(**kwargs)
1697 strings = [t % params for t in templates]
1698 return strings
1700 # hand through templates dict
1701 def __getitem__(self, template_name):
1702 return self.templates[template_name]
1704 def __setitem__(self, template_name, template):
1705 self.templates[template_name] = template
1707 def __contains__(self, template_name):
1708 return template_name in self.templates
1710 def __iter__(self):
1711 return iter(self.templates)
1713 def __len__(self):
1714 return len(self.templates)
1716 def __delitem__(self, template_name):
1717 del(self.templates[template_name])
1719 def _simple_fill(self, template_names, **kwargs):
1720 templates = [self.templates[n] for n in template_names]
1721 return self.fill(templates, **kwargs)
1723 def __getattr__(self, template_names):
1724 if [n for n in template_names if n not in self.templates]:
1725 raise AttributeError(template_names)
1727 def f(**kwargs):
1728 return self._simple_fill(template_names, **kwargs)
1730 return f
1733def nice_value(x):
1734 '''
1735 Round ``x`` to nice value.
1736 '''
1738 exp = 1.0
1739 sign = 1
1740 if x < 0.0:
1741 x = -x
1742 sign = -1
1743 while x >= 1.0:
1744 x /= 10.0
1745 exp *= 10.0
1746 while x < 0.1:
1747 x *= 10.0
1748 exp /= 10.0
1750 if x >= 0.75:
1751 return sign * 1.0 * exp
1752 if x >= 0.375:
1753 return sign * 0.5 * exp
1754 if x >= 0.225:
1755 return sign * 0.25 * exp
1756 if x >= 0.15:
1757 return sign * 0.2 * exp
1759 return sign * 0.1 * exp
1762class AutoScaler(object):
1763 '''
1764 Tunable 1D autoscaling based on data range.
1766 Instances of this class may be used to determine nice minima, maxima and
1767 increments for ax annotations, as well as suitable common exponents for
1768 notation.
1770 The autoscaling process is guided by the following public attributes:
1772 .. py:attribute:: approx_ticks
1774 Approximate number of increment steps (tickmarks) to generate.
1776 .. py:attribute:: mode
1778 Mode of operation: one of ``'auto'``, ``'min-max'``, ``'0-max'``,
1779 ``'min-0'``, ``'symmetric'`` or ``'off'``.
1781 ================ ==================================================
1782 mode description
1783 ================ ==================================================
1784 ``'auto'``: Look at data range and choose one of the choices
1785 below.
1786 ``'min-max'``: Output range is selected to include data range.
1787 ``'0-max'``: Output range shall start at zero and end at data
1788 max.
1789 ``'min-0'``: Output range shall start at data min and end at
1790 zero.
1791 ``'symmetric'``: Output range shall by symmetric by zero.
1792 ``'off'``: Similar to ``'min-max'``, but snap and space are
1793 disabled, such that the output range always
1794 exactly matches the data range.
1795 ================ ==================================================
1797 .. py:attribute:: exp
1799 If defined, override automatically determined exponent for notation
1800 by the given value.
1802 .. py:attribute:: snap
1804 If set to True, snap output range to multiples of increment. This
1805 parameter has no effect, if mode is set to ``'off'``.
1807 .. py:attribute:: inc
1809 If defined, override automatically determined tick increment by the
1810 given value.
1812 .. py:attribute:: space
1814 Add some padding to the range. The value given, is the fraction by
1815 which the output range is increased on each side. If mode is
1816 ``'0-max'`` or ``'min-0'``, the end at zero is kept fixed at zero.
1817 This parameter has no effect if mode is set to ``'off'``.
1819 .. py:attribute:: exp_factor
1821 Exponent of notation is chosen to be a multiple of this value.
1823 .. py:attribute:: no_exp_interval:
1825 Range of exponent, for which no exponential notation is allowed.
1827 '''
1829 def __init__(
1830 self,
1831 approx_ticks=7.0,
1832 mode='auto',
1833 exp=None,
1834 snap=False,
1835 inc=None,
1836 space=0.0,
1837 exp_factor=3,
1838 no_exp_interval=(-3, 5)):
1840 '''
1841 Create new AutoScaler instance.
1843 The parameters are described in the AutoScaler documentation.
1844 '''
1846 self.approx_ticks = approx_ticks
1847 self.mode = mode
1848 self.exp = exp
1849 self.snap = snap
1850 self.inc = inc
1851 self.space = space
1852 self.exp_factor = exp_factor
1853 self.no_exp_interval = no_exp_interval
1855 def make_scale(self, data_range, override_mode=None):
1857 '''
1858 Get nice minimum, maximum and increment for given data range.
1860 Returns ``(minimum, maximum, increment)`` or ``(maximum, minimum,
1861 -increment)``, depending on whether data_range is ``(data_min,
1862 data_max)`` or ``(data_max, data_min)``. If ``override_mode`` is
1863 defined, the mode attribute is temporarily overridden by the given
1864 value.
1865 '''
1867 data_min = min(data_range)
1868 data_max = max(data_range)
1870 is_reverse = (data_range[0] > data_range[1])
1872 a = self.mode
1873 if self.mode == 'auto':
1874 a = self.guess_autoscale_mode(data_min, data_max)
1876 if override_mode is not None:
1877 a = override_mode
1879 mi, ma = 0, 0
1880 if a == 'off':
1881 mi, ma = data_min, data_max
1882 elif a == '0-max':
1883 mi = 0.0
1884 if data_max > 0.0:
1885 ma = data_max
1886 else:
1887 ma = 1.0
1888 elif a == 'min-0':
1889 ma = 0.0
1890 if data_min < 0.0:
1891 mi = data_min
1892 else:
1893 mi = -1.0
1894 elif a == 'min-max':
1895 mi, ma = data_min, data_max
1896 elif a == 'symmetric':
1897 m = max(abs(data_min), abs(data_max))
1898 mi = -m
1899 ma = m
1901 nmi = mi
1902 if (mi != 0. or a == 'min-max') and a != 'off':
1903 nmi = mi - self.space*(ma-mi)
1905 nma = ma
1906 if (ma != 0. or a == 'min-max') and a != 'off':
1907 nma = ma + self.space*(ma-mi)
1909 mi, ma = nmi, nma
1911 if mi == ma and a != 'off':
1912 mi -= 1.0
1913 ma += 1.0
1915 # make nice tick increment
1916 if self.inc is not None:
1917 inc = self.inc
1918 else:
1919 if self.approx_ticks > 0.:
1920 inc = nice_value((ma-mi) / self.approx_ticks)
1921 else:
1922 inc = nice_value((ma-mi)*10.)
1924 if inc == 0.0:
1925 inc = 1.0
1927 # snap min and max to ticks if this is wanted
1928 if self.snap and a != 'off':
1929 ma = inc * math.ceil(ma/inc)
1930 mi = inc * math.floor(mi/inc)
1932 if is_reverse:
1933 return ma, mi, -inc
1934 else:
1935 return mi, ma, inc
1937 def make_exp(self, x):
1938 '''
1939 Get nice exponent for notation of ``x``.
1941 For ax annotations, give tick increment as ``x``.
1942 '''
1944 if self.exp is not None:
1945 return self.exp
1947 x = abs(x)
1948 if x == 0.0:
1949 return 0
1951 if 10**self.no_exp_interval[0] <= x <= 10**self.no_exp_interval[1]:
1952 return 0
1954 return math.floor(math.log10(x)/self.exp_factor)*self.exp_factor
1956 def guess_autoscale_mode(self, data_min, data_max):
1957 '''
1958 Guess mode of operation, based on data range.
1960 Used to map ``'auto'`` mode to ``'0-max'``, ``'min-0'``, ``'min-max'``
1961 or ``'symmetric'``.
1962 '''
1964 a = 'min-max'
1965 if data_min >= 0.0:
1966 if data_min < data_max/2.:
1967 a = '0-max'
1968 else:
1969 a = 'min-max'
1970 if data_max <= 0.0:
1971 if data_max > data_min/2.:
1972 a = 'min-0'
1973 else:
1974 a = 'min-max'
1975 if data_min < 0.0 and data_max > 0.0:
1976 if abs((abs(data_max)-abs(data_min)) /
1977 (abs(data_max)+abs(data_min))) < 0.5:
1978 a = 'symmetric'
1979 else:
1980 a = 'min-max'
1981 return a
1984class Ax(AutoScaler):
1985 '''
1986 Ax description with autoscaling capabilities.
1988 The ax is described by the :py:class:`AutoScaler` public attributes, plus
1989 the following additional attributes (with default values given in
1990 paranthesis):
1992 .. py:attribute:: label
1994 Ax label (without unit).
1996 .. py:attribute:: unit
1998 Physical unit of the data attached to this ax.
2000 .. py:attribute:: scaled_unit
2002 (see below)
2004 .. py:attribute:: scaled_unit_factor
2006 Scaled physical unit and factor between unit and scaled_unit so that
2008 unit = scaled_unit_factor x scaled_unit.
2010 (E.g. if unit is 'm' and data is in the range of nanometers, you may
2011 want to set the scaled_unit to 'nm' and the scaled_unit_factor to
2012 1e9.)
2014 .. py:attribute:: limits
2016 If defined, fix range of ax to limits=(min,max).
2018 .. py:attribute:: masking
2020 If true and if there is a limit on the ax, while calculating ranges,
2021 the data points are masked such that data points outside of this axes
2022 limits are not used to determine the range of another dependant ax.
2024 '''
2026 def __init__(self, label='', unit='', scaled_unit_factor=1.,
2027 scaled_unit='', limits=None, masking=True, **kwargs):
2029 AutoScaler.__init__(self, **kwargs)
2030 self.label = label
2031 self.unit = unit
2032 self.scaled_unit_factor = scaled_unit_factor
2033 self.scaled_unit = scaled_unit
2034 self.limits = limits
2035 self.masking = masking
2037 def label_str(self, exp, unit):
2038 '''
2039 Get label string including the unit and multiplier.
2040 '''
2042 slabel, sunit, sexp = '', '', ''
2043 if self.label:
2044 slabel = self.label
2046 if unit or exp != 0:
2047 if exp != 0:
2048 sexp = '\\327 10@+%i@+' % exp
2049 sunit = '[ %s %s ]' % (sexp, unit)
2050 else:
2051 sunit = '[ %s ]' % unit
2053 p = []
2054 if slabel:
2055 p.append(slabel)
2057 if sunit:
2058 p.append(sunit)
2060 return ' '.join(p)
2062 def make_params(self, data_range, ax_projection=False, override_mode=None,
2063 override_scaled_unit_factor=None):
2065 '''
2066 Get minimum, maximum, increment and label string for ax display.'
2068 Returns minimum, maximum, increment and label string including unit and
2069 multiplier for given data range.
2071 If ``ax_projection`` is True, values suitable to be displayed on the ax
2072 are returned, e.g. min, max and inc are returned in scaled units.
2073 Otherwise the values are returned in the original units, without any
2074 scaling applied.
2075 '''
2077 sf = self.scaled_unit_factor
2079 if override_scaled_unit_factor is not None:
2080 sf = override_scaled_unit_factor
2082 dr_scaled = [sf*x for x in data_range]
2084 mi, ma, inc = self.make_scale(dr_scaled, override_mode=override_mode)
2085 if self.inc is not None:
2086 inc = self.inc*sf
2088 if ax_projection:
2089 exp = self.make_exp(inc)
2090 if sf == 1. and override_scaled_unit_factor is None:
2091 unit = self.unit
2092 else:
2093 unit = self.scaled_unit
2094 label = self.label_str(exp, unit)
2095 return mi/10**exp, ma/10**exp, inc/10**exp, label
2096 else:
2097 label = self.label_str(0, self.unit)
2098 return mi/sf, ma/sf, inc/sf, label
2101class ScaleGuru(Guru):
2103 '''
2104 2D/3D autoscaling and ax annotation facility.
2106 Instances of this class provide automatic determination of plot ranges,
2107 tick increments and scaled annotations, as well as label/unit handling. It
2108 can in particular be used to automatically generate the -R and -B option
2109 arguments, which are required for most GMT commands.
2111 It extends the functionality of the :py:class:`Ax` and
2112 :py:class:`AutoScaler` classes at the level, where it can not be handled
2113 anymore by looking at a single dimension of the dataset's data, e.g.:
2115 * The ability to impose a fixed aspect ratio between two axes.
2117 * Recalculation of data range on non-limited axes, when there are
2118 limits imposed on other axes.
2120 '''
2122 def __init__(self, data_tuples=None, axes=None, aspect=None,
2123 percent_interval=None, copy_from=None):
2125 Guru.__init__(self)
2127 if copy_from:
2128 self.templates = copy.deepcopy(copy_from.templates)
2129 self.axes = copy.deepcopy(copy_from.axes)
2130 self.data_ranges = copy.deepcopy(copy_from.data_ranges)
2131 self.aspect = copy_from.aspect
2133 if percent_interval is not None:
2134 from scipy.stats import scoreatpercentile as scap
2136 self.templates = dict(
2137 R='-R%(xmin)g/%(xmax)g/%(ymin)g/%(ymax)g',
2138 B='-B%(xinc)g:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen',
2139 T='-T%(zmin)g/%(zmax)g/%(zinc)g')
2141 maxdim = 2
2142 if data_tuples:
2143 maxdim = max(maxdim, max([len(dt) for dt in data_tuples]))
2144 else:
2145 if axes:
2146 maxdim = len(axes)
2147 data_tuples = [([],) * maxdim]
2148 if axes is not None:
2149 self.axes = axes
2150 else:
2151 self.axes = [Ax() for i in range(maxdim)]
2153 # sophisticated data-range calculation
2154 data_ranges = [None] * maxdim
2155 for dt_ in data_tuples:
2156 dt = num.asarray(dt_)
2157 in_range = True
2158 for ax, x in zip(self.axes, dt):
2159 if ax.limits and ax.masking:
2160 ax_limits = list(ax.limits)
2161 if ax_limits[0] is None:
2162 ax_limits[0] = -num.inf
2163 if ax_limits[1] is None:
2164 ax_limits[1] = num.inf
2165 in_range = num.logical_and(
2166 in_range,
2167 num.logical_and(ax_limits[0] <= x, x <= ax_limits[1]))
2169 for i, ax, x in zip(range(maxdim), self.axes, dt):
2171 if not ax.limits or None in ax.limits:
2172 if len(x) >= 1:
2173 if in_range is not True:
2174 xmasked = num.where(in_range, x, num.NaN)
2175 if percent_interval is None:
2176 range_this = (
2177 num.nanmin(xmasked),
2178 num.nanmax(xmasked))
2179 else:
2180 xmasked_finite = num.compress(
2181 num.isfinite(xmasked), xmasked)
2182 range_this = (
2183 scap(xmasked_finite,
2184 (100.-percent_interval)/2.),
2185 scap(xmasked_finite,
2186 100.-(100.-percent_interval)/2.))
2187 else:
2188 if percent_interval is None:
2189 range_this = num.nanmin(x), num.nanmax(x)
2190 else:
2191 xmasked_finite = num.compress(
2192 num.isfinite(xmasked), xmasked)
2193 range_this = (
2194 scap(xmasked_finite,
2195 (100.-percent_interval)/2.),
2196 scap(xmasked_finite,
2197 100.-(100.-percent_interval)/2.))
2198 else:
2199 range_this = (0., 1.)
2201 if ax.limits:
2202 if ax.limits[0] is not None:
2203 range_this = ax.limits[0], max(ax.limits[0],
2204 range_this[1])
2206 if ax.limits[1] is not None:
2207 range_this = min(ax.limits[1],
2208 range_this[0]), ax.limits[1]
2210 else:
2211 range_this = ax.limits
2213 if data_ranges[i] is None and range_this[0] <= range_this[1]:
2214 data_ranges[i] = range_this
2215 else:
2216 mi, ma = range_this
2217 if data_ranges[i] is not None:
2218 mi = min(data_ranges[i][0], mi)
2219 ma = max(data_ranges[i][1], ma)
2221 data_ranges[i] = (mi, ma)
2223 for i in range(len(data_ranges)):
2224 if data_ranges[i] is None or not (
2225 num.isfinite(data_ranges[i][0])
2226 and num.isfinite(data_ranges[i][1])):
2228 data_ranges[i] = (0., 1.)
2230 self.data_ranges = data_ranges
2231 self.aspect = aspect
2233 def copy(self):
2234 return ScaleGuru(copy_from=self)
2236 def get_params(self, ax_projection=False):
2238 '''
2239 Get dict with output parameters.
2241 For each data dimension, ax minimum, maximum, increment and a label
2242 string (including unit and exponential factor) are determined. E.g. in
2243 for the first dimension the output dict will contain the keys
2244 ``'xmin'``, ``'xmax'``, ``'xinc'``, and ``'xlabel'``.
2246 Normally, values corresponding to the scaling of the raw data are
2247 produced, but if ``ax_projection`` is ``True``, values which are
2248 suitable to be printed on the axes are returned. This means that in the
2249 latter case, the :py:attr:`Ax.scaled_unit` and
2250 :py:attr:`Ax.scaled_unit_factor` attributes as set on the axes are
2251 respected and that a common 10^x factor is factored out and put to the
2252 label string.
2253 '''
2255 xmi, xma, xinc, xlabel = self.axes[0].make_params(
2256 self.data_ranges[0], ax_projection)
2257 ymi, yma, yinc, ylabel = self.axes[1].make_params(
2258 self.data_ranges[1], ax_projection)
2259 if len(self.axes) > 2:
2260 zmi, zma, zinc, zlabel = self.axes[2].make_params(
2261 self.data_ranges[2], ax_projection)
2263 # enforce certain aspect, if needed
2264 if self.aspect is not None:
2265 xwid = xma-xmi
2266 ywid = yma-ymi
2267 if ywid < xwid*self.aspect:
2268 ymi -= (xwid*self.aspect - ywid)*0.5
2269 yma += (xwid*self.aspect - ywid)*0.5
2270 ymi, yma, yinc, ylabel = self.axes[1].make_params(
2271 (ymi, yma), ax_projection, override_mode='off',
2272 override_scaled_unit_factor=1.)
2274 elif xwid < ywid/self.aspect:
2275 xmi -= (ywid/self.aspect - xwid)*0.5
2276 xma += (ywid/self.aspect - xwid)*0.5
2277 xmi, xma, xinc, xlabel = self.axes[0].make_params(
2278 (xmi, xma), ax_projection, override_mode='off',
2279 override_scaled_unit_factor=1.)
2281 params = dict(xmin=xmi, xmax=xma, xinc=xinc, xlabel=xlabel,
2282 ymin=ymi, ymax=yma, yinc=yinc, ylabel=ylabel)
2283 if len(self.axes) > 2:
2284 params.update(dict(zmin=zmi, zmax=zma, zinc=zinc, zlabel=zlabel))
2286 return params
2289class GumSpring(object):
2291 '''
2292 Sizing policy implementing a minimal size, plus a desire to grow.
2293 '''
2295 def __init__(self, minimal=None, grow=None):
2296 self.minimal = minimal
2297 if grow is None:
2298 if minimal is None:
2299 self.grow = 1.0
2300 else:
2301 self.grow = 0.0
2302 else:
2303 self.grow = grow
2304 self.value = 1.0
2306 def get_minimal(self):
2307 if self.minimal is not None:
2308 return self.minimal
2309 else:
2310 return 0.0
2312 def get_grow(self):
2313 return self.grow
2315 def set_value(self, value):
2316 self.value = value
2318 def get_value(self):
2319 return self.value
2322def distribute(sizes, grows, space):
2323 sizes = list(sizes)
2324 gsum = sum(grows)
2325 if gsum > 0.0:
2326 for i in range(len(sizes)):
2327 sizes[i] += space*grows[i]/gsum
2328 return sizes
2331class Widget(Guru):
2333 '''
2334 Base class of the gmtpy layout system.
2336 The Widget class provides the basic functionality for the nesting and
2337 placing of elements on the output page, and maintains the sizing policies
2338 of each element. Each of the layouts defined in gmtpy is itself a Widget.
2340 Sizing of the widget is controlled by :py:meth:`get_min_size` and
2341 :py:meth:`get_grow` which should be overloaded in derived classes. The
2342 basic behaviour of a Widget instance is to have a vertical and a horizontal
2343 minimum size which default to zero, as well as a vertical and a horizontal
2344 desire to grow, represented by floats, which default to 1.0. Additionally
2345 an aspect ratio constraint may be imposed on the Widget.
2347 After layouting, the widget provides its width, height, x-offset and
2348 y-offset in various ways. Via the Guru interface (see :py:class:`Guru`
2349 class), templates for the -X, -Y and -J option arguments used by GMT
2350 arguments are provided. The defaults are suitable for plotting of linear
2351 (-JX) plots. Other projections can be selected by giving an appropriate 'J'
2352 template, or by manual construction of the -J option, e.g. by utilizing the
2353 :py:meth:`width` and :py:meth:`height` methods. The :py:meth:`bbox` method
2354 can be used to create a PostScript bounding box from the widgets border,
2355 e.g. for use in the :py:meth:`save` method of :py:class:`GMT` instances.
2357 The convention is, that all sizes are given in PostScript points.
2358 Conversion factors are provided as constants :py:const:`inch` and
2359 :py:const:`cm` in the gmtpy module.
2360 '''
2362 def __init__(self, horizontal=None, vertical=None, parent=None):
2364 '''
2365 Create new widget.
2366 '''
2368 Guru.__init__(self)
2370 self.templates = dict(
2371 X='-Xa%(xoffset)gp',
2372 Y='-Ya%(yoffset)gp',
2373 J='-JX%(width)gp/%(height)gp')
2375 if horizontal is None:
2376 self.horizontal = GumSpring()
2377 else:
2378 self.horizontal = horizontal
2380 if vertical is None:
2381 self.vertical = GumSpring()
2382 else:
2383 self.vertical = vertical
2385 self.aspect = None
2386 self.parent = parent
2387 self.dirty = True
2389 def set_parent(self, parent):
2391 '''
2392 Set the parent widget.
2394 This method should not be called directly. The :py:meth:`set_widget`
2395 methods are responsible for calling this.
2396 '''
2398 self.parent = parent
2399 self.dirtyfy()
2401 def get_parent(self):
2403 '''
2404 Get the widgets parent widget.
2405 '''
2407 return self.parent
2409 def get_root(self):
2411 '''
2412 Get the root widget in the layout hierarchy.
2413 '''
2415 if self.parent is not None:
2416 return self.get_parent()
2417 else:
2418 return self
2420 def set_horizontal(self, minimal=None, grow=None):
2422 '''
2423 Set the horizontal sizing policy of the Widget.
2426 :param minimal: new minimal width of the widget
2427 :param grow: new horizontal grow disire of the widget
2428 '''
2430 self.horizontal = GumSpring(minimal, grow)
2431 self.dirtyfy()
2433 def get_horizontal(self):
2434 return self.horizontal.get_minimal(), self.horizontal.get_grow()
2436 def set_vertical(self, minimal=None, grow=None):
2438 '''
2439 Set the horizontal sizing policy of the Widget.
2441 :param minimal: new minimal height of the widget
2442 :param grow: new vertical grow disire of the widget
2443 '''
2445 self.vertical = GumSpring(minimal, grow)
2446 self.dirtyfy()
2448 def get_vertical(self):
2449 return self.vertical.get_minimal(), self.vertical.get_grow()
2451 def set_aspect(self, aspect=None):
2453 '''
2454 Set aspect constraint on the widget.
2456 The aspect is given as height divided by width.
2457 '''
2459 self.aspect = aspect
2460 self.dirtyfy()
2462 def set_policy(self, minimal=(None, None), grow=(None, None), aspect=None):
2464 '''
2465 Shortcut to set sizing and aspect constraints in a single method
2466 call.
2467 '''
2469 self.set_horizontal(minimal[0], grow[0])
2470 self.set_vertical(minimal[1], grow[1])
2471 self.set_aspect(aspect)
2473 def get_policy(self):
2474 mh, gh = self.get_horizontal()
2475 mv, gv = self.get_vertical()
2476 return (mh, mv), (gh, gv), self.aspect
2478 def legalize(self, size, offset):
2480 '''
2481 Get legal size for widget.
2483 Returns: (new_size, new_offset)
2485 Given a box as ``size`` and ``offset``, return ``new_size`` and
2486 ``new_offset``, such that the widget's sizing and aspect constraints
2487 are fullfilled. The returned box is centered on the given input box.
2488 '''
2490 sh, sv = size
2491 oh, ov = offset
2492 shs, svs = Widget.get_min_size(self)
2493 ghs, gvs = Widget.get_grow(self)
2495 if ghs == 0.0:
2496 oh += (sh-shs)/2.
2497 sh = shs
2499 if gvs == 0.0:
2500 ov += (sv-svs)/2.
2501 sv = svs
2503 if self.aspect is not None:
2504 if sh > sv/self.aspect:
2505 oh += (sh-sv/self.aspect)/2.
2506 sh = sv/self.aspect
2507 if sv > sh*self.aspect:
2508 ov += (sv-sh*self.aspect)/2.
2509 sv = sh*self.aspect
2511 return (sh, sv), (oh, ov)
2513 def get_min_size(self):
2515 '''
2516 Get minimum size of widget.
2518 Used by the layout managers. Should be overloaded in derived classes.
2519 '''
2521 mh, mv = self.horizontal.get_minimal(), self.vertical.get_minimal()
2522 if self.aspect is not None:
2523 if mv == 0.0:
2524 return mh, mh*self.aspect
2525 elif mh == 0.0:
2526 return mv/self.aspect, mv
2527 return mh, mv
2529 def get_grow(self):
2531 '''
2532 Get widget's desire to grow.
2534 Used by the layout managers. Should be overloaded in derived classes.
2535 '''
2537 return self.horizontal.get_grow(), self.vertical.get_grow()
2539 def set_size(self, size, offset):
2541 '''
2542 Set the widget's current size.
2544 Should not be called directly. It is the layout manager's
2545 responsibility to call this.
2546 '''
2548 (sh, sv), inner_offset = self.legalize(size, offset)
2549 self.offset = inner_offset
2550 self.horizontal.set_value(sh)
2551 self.vertical.set_value(sv)
2552 self.dirty = False
2554 def __str__(self):
2556 def indent(ind, str):
2557 return ('\n'+ind).join(str.splitlines())
2558 size, offset = self.get_size()
2559 s = "%s (%g x %g) (%g, %g)\n" % ((self.__class__,) + size + offset)
2560 children = self.get_children()
2561 if children:
2562 s += '\n'.join([' ' + indent(' ', str(c)) for c in children])
2563 return s
2565 def policies_debug_str(self):
2567 def indent(ind, str):
2568 return ('\n'+ind).join(str.splitlines())
2569 mins, grows, aspect = self.get_policy()
2570 s = "%s: minimum=(%s, %s), grow=(%s, %s), aspect=%s\n" % (
2571 (self.__class__,) + mins+grows+(aspect,))
2573 children = self.get_children()
2574 if children:
2575 s += '\n'.join([' ' + indent(
2576 ' ', c.policies_debug_str()) for c in children])
2577 return s
2579 def get_corners(self, descend=False):
2581 '''
2582 Get coordinates of the corners of the widget.
2584 Returns list with coordinate tuples.
2586 If ``descend`` is True, the returned list will contain corner
2587 coordinates of all sub-widgets.
2588 '''
2590 self.do_layout()
2591 (sh, sv), (oh, ov) = self.get_size()
2592 corners = [(oh, ov), (oh+sh, ov), (oh+sh, ov+sv), (oh, ov+sv)]
2593 if descend:
2594 for child in self.get_children():
2595 corners.extend(child.get_corners(descend=True))
2596 return corners
2598 def get_sizes(self):
2600 '''
2601 Get sizes of this widget and all it's children.
2603 Returns a list with size tuples.
2604 '''
2605 self.do_layout()
2606 sizes = [self.get_size()]
2607 for child in self.get_children():
2608 sizes.extend(child.get_sizes())
2609 return sizes
2611 def do_layout(self):
2613 '''
2614 Triggers layouting of the widget hierarchy, if needed.
2615 '''
2617 if self.parent is not None:
2618 return self.parent.do_layout()
2620 if not self.dirty:
2621 return
2623 sh, sv = self.get_min_size()
2624 gh, gv = self.get_grow()
2625 if sh == 0.0 and gh != 0.0:
2626 sh = 15.*cm
2627 if sv == 0.0 and gv != 0.0:
2628 sv = 15.*cm*gv/gh * 1./golden_ratio
2629 self.set_size((sh, sv), (0., 0.))
2631 def get_children(self):
2633 '''
2634 Get sub-widgets contained in this widget.
2636 Returns a list of widgets.
2637 '''
2639 return []
2641 def get_size(self):
2643 '''
2644 Get current size and position of the widget.
2646 Triggers layouting and returns
2647 ``((width, height), (xoffset, yoffset))``
2648 '''
2650 self.do_layout()
2651 return (self.horizontal.get_value(),
2652 self.vertical.get_value()), self.offset
2654 def get_params(self):
2656 '''
2657 Get current size and position of the widget.
2659 Triggers layouting and returns dict with keys ``'xoffset'``,
2660 ``'yoffset'``, ``'width'`` and ``'height'``.
2661 '''
2663 self.do_layout()
2664 (w, h), (xo, yo) = self.get_size()
2665 return dict(xoffset=xo, yoffset=yo, width=w, height=h,
2666 width_m=w/_units['m'])
2668 def width(self):
2670 '''
2671 Get current width of the widget.
2673 Triggers layouting and returns width.
2674 '''
2676 self.do_layout()
2677 return self.horizontal.get_value()
2679 def height(self):
2681 '''
2682 Get current height of the widget.
2684 Triggers layouting and return height.
2685 '''
2687 self.do_layout()
2688 return self.vertical.get_value()
2690 def bbox(self):
2692 '''
2693 Get PostScript bounding box for this widget.
2695 Triggers layouting and returns values suitable to create PS bounding
2696 box, representing the widgets current size and position.
2697 '''
2699 self.do_layout()
2700 return (self.offset[0], self.offset[1], self.offset[0]+self.width(),
2701 self.offset[1]+self.height())
2703 def dirtyfy(self):
2705 '''
2706 Set dirty flag on top level widget in the hierarchy.
2708 Called by various methods, to indicate, that the widget hierarchy needs
2709 new layouting.
2710 '''
2712 if self.parent is not None:
2713 self.parent.dirtyfy()
2715 self.dirty = True
2718class CenterLayout(Widget):
2720 '''
2721 A layout manager which centers its single child widget.
2723 The child widget may be oversized.
2724 '''
2726 def __init__(self, horizontal=None, vertical=None):
2727 Widget.__init__(self, horizontal, vertical)
2728 self.content = Widget(horizontal=GumSpring(grow=1.),
2729 vertical=GumSpring(grow=1.), parent=self)
2731 def get_min_size(self):
2732 shs, svs = Widget.get_min_size(self)
2733 sh, sv = self.content.get_min_size()
2734 return max(shs, sh), max(svs, sv)
2736 def get_grow(self):
2737 ghs, gvs = Widget.get_grow(self)
2738 gh, gv = self.content.get_grow()
2739 return gh*ghs, gv*gvs
2741 def set_size(self, size, offset):
2742 (sh, sv), (oh, ov) = self.legalize(size, offset)
2744 shc, svc = self.content.get_min_size()
2745 ghc, gvc = self.content.get_grow()
2746 if ghc != 0.:
2747 shc = sh
2748 if gvc != 0.:
2749 svc = sv
2750 ohc = oh+(sh-shc)/2.
2751 ovc = ov+(sv-svc)/2.
2753 self.content.set_size((shc, svc), (ohc, ovc))
2754 Widget.set_size(self, (sh, sv), (oh, ov))
2756 def set_widget(self, widget=None):
2758 '''
2759 Set the child widget, which shall be centered.
2760 '''
2762 if widget is None:
2763 widget = Widget()
2765 self.content = widget
2767 widget.set_parent(self)
2769 def get_widget(self):
2770 return self.content
2772 def get_children(self):
2773 return [self.content]
2776class FrameLayout(Widget):
2778 '''
2779 A layout manager containing a center widget sorrounded by four margin
2780 widgets.
2782 ::
2784 +---------------------------+
2785 | top |
2786 +---------------------------+
2787 | | | |
2788 | left | center | right |
2789 | | | |
2790 +---------------------------+
2791 | bottom |
2792 +---------------------------+
2794 This layout manager does a little bit of extra effort to maintain the
2795 aspect constraint of the center widget, if this is set. It does so, by
2796 allowing for a bit more flexibility in the sizing of the margins. Two
2797 shortcut methods are provided to set the margin sizes in one shot:
2798 :py:meth:`set_fixed_margins` and :py:meth:`set_min_margins`. The first sets
2799 the margins to fixed sizes, while the second gives them a minimal size and
2800 a (neglectably) small desire to grow. Using the latter may be useful when
2801 setting an aspect constraint on the center widget, because this way the
2802 maximum size of the center widget may be controlled without creating empty
2803 spaces between the widgets.
2804 '''
2806 def __init__(self, horizontal=None, vertical=None):
2807 Widget.__init__(self, horizontal, vertical)
2808 mw = 3.*cm
2809 self.left = Widget(
2810 horizontal=GumSpring(grow=0.15, minimal=mw), parent=self)
2811 self.right = Widget(
2812 horizontal=GumSpring(grow=0.15, minimal=mw), parent=self)
2813 self.top = Widget(
2814 vertical=GumSpring(grow=0.15, minimal=mw/golden_ratio),
2815 parent=self)
2816 self.bottom = Widget(
2817 vertical=GumSpring(grow=0.15, minimal=mw/golden_ratio),
2818 parent=self)
2819 self.center = Widget(
2820 horizontal=GumSpring(grow=0.7), vertical=GumSpring(grow=0.7),
2821 parent=self)
2823 def set_fixed_margins(self, left, right, top, bottom):
2824 '''
2825 Give margins fixed size constraints.
2826 '''
2828 self.left.set_horizontal(left, 0)
2829 self.right.set_horizontal(right, 0)
2830 self.top.set_vertical(top, 0)
2831 self.bottom.set_vertical(bottom, 0)
2833 def set_min_margins(self, left, right, top, bottom, grow=0.0001):
2834 '''
2835 Give margins a minimal size and the possibility to grow.
2837 The desire to grow is set to a very small number.
2838 '''
2839 self.left.set_horizontal(left, grow)
2840 self.right.set_horizontal(right, grow)
2841 self.top.set_vertical(top, grow)
2842 self.bottom.set_vertical(bottom, grow)
2844 def get_min_size(self):
2845 shs, svs = Widget.get_min_size(self)
2847 sl, sr, st, sb, sc = [x.get_min_size() for x in (
2848 self.left, self.right, self.top, self.bottom, self.center)]
2849 gl, gr, gt, gb, gc = [x.get_grow() for x in (
2850 self.left, self.right, self.top, self.bottom, self.center)]
2852 shsum = sl[0]+sr[0]+sc[0]
2853 svsum = st[1]+sb[1]+sc[1]
2855 # prevent widgets from collapsing
2856 for s, g in ((sl, gl), (sr, gr), (sc, gc)):
2857 if s[0] == 0.0 and g[0] != 0.0:
2858 shsum += 0.1*cm
2860 for s, g in ((st, gt), (sb, gb), (sc, gc)):
2861 if s[1] == 0.0 and g[1] != 0.0:
2862 svsum += 0.1*cm
2864 sh = max(shs, shsum)
2865 sv = max(svs, svsum)
2867 return sh, sv
2869 def get_grow(self):
2870 ghs, gvs = Widget.get_grow(self)
2871 gh = (self.left.get_grow()[0] +
2872 self.right.get_grow()[0] +
2873 self.center.get_grow()[0]) * ghs
2874 gv = (self.top.get_grow()[1] +
2875 self.bottom.get_grow()[1] +
2876 self.center.get_grow()[1]) * gvs
2877 return gh, gv
2879 def set_size(self, size, offset):
2880 (sh, sv), (oh, ov) = self.legalize(size, offset)
2882 sl, sr, st, sb, sc = [x.get_min_size() for x in (
2883 self.left, self.right, self.top, self.bottom, self.center)]
2884 gl, gr, gt, gb, gc = [x.get_grow() for x in (
2885 self.left, self.right, self.top, self.bottom, self.center)]
2887 ah = sh - (sl[0]+sr[0]+sc[0])
2888 av = sv - (st[1]+sb[1]+sc[1])
2890 if ah < 0.0:
2891 raise GmtPyError("Container not wide enough for contents "
2892 "(FrameLayout, available: %g cm, needed: %g cm)"
2893 % (sh/cm, (sl[0]+sr[0]+sc[0])/cm))
2894 if av < 0.0:
2895 raise GmtPyError("Container not high enough for contents "
2896 "(FrameLayout, available: %g cm, needed: %g cm)"
2897 % (sv/cm, (st[1]+sb[1]+sc[1])/cm))
2899 slh, srh, sch = distribute((sl[0], sr[0], sc[0]),
2900 (gl[0], gr[0], gc[0]), ah)
2901 stv, sbv, scv = distribute((st[1], sb[1], sc[1]),
2902 (gt[1], gb[1], gc[1]), av)
2904 if self.center.aspect is not None:
2905 ahm = sh - (sl[0]+sr[0] + scv/self.center.aspect)
2906 avm = sv - (st[1]+sb[1] + sch*self.center.aspect)
2907 if 0.0 < ahm < ah:
2908 slh, srh, sch = distribute(
2909 (sl[0], sr[0], scv/self.center.aspect),
2910 (gl[0], gr[0], 0.0), ahm)
2912 elif 0.0 < avm < av:
2913 stv, sbv, scv = distribute((st[1], sb[1],
2914 sch*self.center.aspect),
2915 (gt[1], gb[1], 0.0), avm)
2917 ah = sh - (slh+srh+sch)
2918 av = sv - (stv+sbv+scv)
2920 oh += ah/2.
2921 ov += av/2.
2922 sh -= ah
2923 sv -= av
2925 self.left.set_size((slh, scv), (oh, ov+sbv))
2926 self.right.set_size((srh, scv), (oh+slh+sch, ov+sbv))
2927 self.top.set_size((sh, stv), (oh, ov+sbv+scv))
2928 self.bottom.set_size((sh, sbv), (oh, ov))
2929 self.center.set_size((sch, scv), (oh+slh, ov+sbv))
2930 Widget.set_size(self, (sh, sv), (oh, ov))
2932 def set_widget(self, which='center', widget=None):
2934 '''
2935 Set one of the sub-widgets.
2937 ``which`` should be one of ``'left'``, ``'right'``, ``'top'``,
2938 ``'bottom'`` or ``'center'``.
2939 '''
2941 if widget is None:
2942 widget = Widget()
2944 if which in ('left', 'right', 'top', 'bottom', 'center'):
2945 self.__dict__[which] = widget
2946 else:
2947 raise GmtPyError('No such sub-widget: %s' % which)
2949 widget.set_parent(self)
2951 def get_widget(self, which='center'):
2953 '''
2954 Get one of the sub-widgets.
2956 ``which`` should be one of ``'left'``, ``'right'``, ``'top'``,
2957 ``'bottom'`` or ``'center'``.
2958 '''
2960 if which in ('left', 'right', 'top', 'bottom', 'center'):
2961 return self.__dict__[which]
2962 else:
2963 raise GmtPyError('No such sub-widget: %s' % which)
2965 def get_children(self):
2966 return [self.left, self.right, self.top, self.bottom, self.center]
2969class GridLayout(Widget):
2971 '''
2972 A layout manager which arranges its sub-widgets in a grid.
2974 The grid spacing is flexible and based on the sizing policies of the
2975 contained sub-widgets. If an equidistant grid is needed, the sizing
2976 policies of the sub-widgets have to be set equally.
2978 The height of each row and the width of each column is derived from the
2979 sizing policy of the largest sub-widget in the row or column in question.
2980 The algorithm is not very sophisticated, so conflicting sizing policies
2981 might not be resolved optimally.
2982 '''
2984 def __init__(self, nx=2, ny=2, horizontal=None, vertical=None):
2986 '''
2987 Create new grid layout with ``nx`` columns and ``ny`` rows.
2988 '''
2990 Widget.__init__(self, horizontal, vertical)
2991 self.grid = []
2992 for iy in range(ny):
2993 row = []
2994 for ix in range(nx):
2995 w = Widget(parent=self)
2996 row.append(w)
2998 self.grid.append(row)
3000 def sub_min_sizes_as_array(self):
3001 esh = num.array(
3002 [[w.get_min_size()[0] for w in row] for row in self.grid],
3003 dtype=float)
3004 esv = num.array(
3005 [[w.get_min_size()[1] for w in row] for row in self.grid],
3006 dtype=float)
3007 return esh, esv
3009 def sub_grows_as_array(self):
3010 egh = num.array(
3011 [[w.get_grow()[0] for w in row] for row in self.grid],
3012 dtype=float)
3013 egv = num.array(
3014 [[w.get_grow()[1] for w in row] for row in self.grid],
3015 dtype=float)
3016 return egh, egv
3018 def get_min_size(self):
3019 sh, sv = Widget.get_min_size(self)
3020 esh, esv = self.sub_min_sizes_as_array()
3021 if esh.size != 0:
3022 sh = max(sh, num.sum(esh.max(0)))
3023 if esv.size != 0:
3024 sv = max(sv, num.sum(esv.max(1)))
3025 return sh, sv
3027 def get_grow(self):
3028 ghs, gvs = Widget.get_grow(self)
3029 egh, egv = self.sub_grows_as_array()
3030 if egh.size != 0:
3031 gh = num.sum(egh.max(0))*ghs
3032 else:
3033 gh = 1.0
3034 if egv.size != 0:
3035 gv = num.sum(egv.max(1))*gvs
3036 else:
3037 gv = 1.0
3038 return gh, gv
3040 def set_size(self, size, offset):
3041 (sh, sv), (oh, ov) = self.legalize(size, offset)
3042 esh, esv = self.sub_min_sizes_as_array()
3043 egh, egv = self.sub_grows_as_array()
3045 # available additional space
3046 empty = esh.size == 0
3048 if not empty:
3049 ah = sh - num.sum(esh.max(0))
3050 av = sv - num.sum(esv.max(1))
3051 else:
3052 av = sv
3053 ah = sh
3055 if ah < 0.0:
3056 raise GmtPyError("Container not wide enough for contents "
3057 "(GridLayout, available: %g cm, needed: %g cm)"
3058 % (sh/cm, (num.sum(esh.max(0)))/cm))
3059 if av < 0.0:
3060 raise GmtPyError("Container not high enough for contents "
3061 "(GridLayout, available: %g cm, needed: %g cm)"
3062 % (sv/cm, (num.sum(esv.max(1)))/cm))
3064 nx, ny = esh.shape
3066 if not empty:
3067 # distribute additional space on rows and columns
3068 # according to grow weights and minimal sizes
3069 gsh = egh.sum(1)[:, num.newaxis].repeat(ny, axis=1)
3070 nesh = esh.copy()
3071 nesh += num.where(gsh > 0.0, ah*egh/gsh, 0.0)
3073 nsh = num.maximum(nesh.max(0), esh.max(0))
3075 gsv = egv.sum(0)[num.newaxis, :].repeat(nx, axis=0)
3076 nesv = esv.copy()
3077 nesv += num.where(gsv > 0.0, av*egv/gsv, 0.0)
3078 nsv = num.maximum(nesv.max(1), esv.max(1))
3080 ah = sh - sum(nsh)
3081 av = sv - sum(nsv)
3083 oh += ah/2.
3084 ov += av/2.
3085 sh -= ah
3086 sv -= av
3088 # resize child widgets
3089 neov = ov + sum(nsv)
3090 for row, nesv in zip(self.grid, nsv):
3091 neov -= nesv
3092 neoh = oh
3093 for w, nesh in zip(row, nsh):
3094 w.set_size((nesh, nesv), (neoh, neov))
3095 neoh += nesh
3097 Widget.set_size(self, (sh, sv), (oh, ov))
3099 def set_widget(self, ix, iy, widget=None):
3101 '''
3102 Set one of the sub-widgets.
3104 Sets the sub-widget in column ``ix`` and row ``iy``. The indices are
3105 counted from zero.
3106 '''
3108 if widget is None:
3109 widget = Widget()
3111 self.grid[iy][ix] = widget
3112 widget.set_parent(self)
3114 def get_widget(self, ix, iy):
3116 '''
3117 Get one of the sub-widgets.
3119 Gets the sub-widget from column ``ix`` and row ``iy``. The indices are
3120 counted from zero.
3121 '''
3123 return self.grid[iy][ix]
3125 def get_children(self):
3126 children = []
3127 for row in self.grid:
3128 children.extend(row)
3130 return children
3133def is_gmt5(version='newest'):
3134 return get_gmt_installation(version)['version'][0] == '5'
3137def aspect_for_projection(gmtversion, *args, **kwargs):
3139 gmt = GMT(version=gmtversion, eps_mode=True)
3141 if gmt.is_gmt5():
3142 gmt.psbasemap('-B+gblack', finish=True, *args, **kwargs)
3143 fn = gmt.tempfilename('test.eps')
3144 gmt.save(fn, crop_eps_mode=True)
3145 with open(fn, 'rb') as f:
3146 s = f.read()
3148 l, b, r, t = get_bbox(s)
3149 else:
3150 gmt.psbasemap('-G0', finish=True, *args, **kwargs)
3151 l, b, r, t = gmt.bbox()
3153 return (t-b)/(r-l)
3156def text_box(
3157 text, font=0, font_size=12., angle=0, gmtversion='newest', **kwargs):
3159 gmt = GMT(version=gmtversion)
3160 if gmt.is_gmt5():
3161 row = [0, 0, text]
3162 farg = ['-F+f%gp,%s,%s+j%s' % (font_size, font, 'black', 'BL')]
3163 else:
3164 row = [0, 0, font_size, 0, font, 'BL', text]
3165 farg = []
3167 gmt.pstext(
3168 in_rows=[row],
3169 finish=True,
3170 R=(0, 1, 0, 1),
3171 J='x10p',
3172 N=True,
3173 *farg,
3174 **kwargs)
3176 fn = gmt.tempfilename() + '.ps'
3177 gmt.save(fn)
3179 (_, stderr) = subprocess.Popen(
3180 ['gs', '-q', '-dNOPAUSE', '-dBATCH', '-r720', '-sDEVICE=bbox', fn],
3181 stderr=subprocess.PIPE).communicate()
3183 dx, dy = None, None
3184 for line in stderr.splitlines():
3185 if line.startswith(b'%%HiResBoundingBox:'):
3186 l, b, r, t = [float(x) for x in line.split()[-4:]]
3187 dx, dy = r-l, t-b
3188 break
3190 return dx, dy
3193class TableLiner(object):
3194 '''
3195 Utility class to turn tables into lines.
3196 '''
3198 def __init__(self, in_columns=None, in_rows=None, encoding='utf-8'):
3199 self.in_columns = in_columns
3200 self.in_rows = in_rows
3201 self.encoding = encoding
3203 def __iter__(self):
3204 if self.in_columns is not None:
3205 for row in zip(*self.in_columns):
3206 yield (' '.join([newstr(x) for x in row])+'\n').encode(
3207 self.encoding)
3209 if self.in_rows is not None:
3210 for row in self.in_rows:
3211 yield (' '.join([newstr(x) for x in row])+'\n').encode(
3212 self.encoding)
3215class LineStreamChopper(object):
3216 '''
3217 File-like object to buffer data.
3218 '''
3220 def __init__(self, liner):
3221 self.chopsize = None
3222 self.liner = liner
3223 self.chop_iterator = None
3224 self.closed = False
3226 def _chopiter(self):
3227 buf = BytesIO()
3228 for line in self.liner:
3229 buf.write(line)
3230 buflen = buf.tell()
3231 if self.chopsize is not None and buflen >= self.chopsize:
3232 buf.seek(0)
3233 while buf.tell() <= buflen-self.chopsize:
3234 yield buf.read(self.chopsize)
3236 newbuf = BytesIO()
3237 newbuf.write(buf.read())
3238 buf.close()
3239 buf = newbuf
3241 yield(buf.getvalue())
3242 buf.close()
3244 def read(self, size=None):
3245 if self.closed:
3246 raise ValueError('Cannot read from closed LineStreamChopper.')
3247 if self.chop_iterator is None:
3248 self.chopsize = size
3249 self.chop_iterator = self._chopiter()
3251 self.chopsize = size
3252 try:
3253 return next(self.chop_iterator)
3254 except StopIteration:
3255 return ''
3257 def close(self):
3258 self.chopsize = None
3259 self.chop_iterator = None
3260 self.closed = True
3262 def flush(self):
3263 pass
3266font_tab = {
3267 0: 'Helvetica',
3268 1: 'Helvetica-Bold',
3269}
3271font_tab_rev = dict((v, k) for (k, v) in font_tab.items())
3274class GMT(object):
3275 '''
3276 A thin wrapper to GMT command execution.
3278 A dict ``config`` may be given to override some of the default GMT
3279 parameters. The ``version`` argument may be used to select a specific GMT
3280 version, which should be used with this GMT instance. The selected
3281 version of GMT has to be installed on the system, must be supported by
3282 gmtpy and gmtpy must know where to find it.
3284 Each instance of this class is used for the task of producing one PS or PDF
3285 output file.
3287 Output of a series of GMT commands is accumulated in memory and can then be
3288 saved as PS or PDF file using the :py:meth:`save` method.
3290 GMT commands are accessed as method calls to instances of this class. See
3291 the :py:meth:`__getattr__` method for details on how the method's
3292 arguments are translated into options and arguments for the GMT command.
3294 Associated with each instance of this class, a temporary directory is
3295 created, where temporary files may be created, and which is automatically
3296 deleted, when the object is destroyed. The :py:meth:`tempfilename` method
3297 may be used to get a random filename in the instance's temporary directory.
3299 Any .gmtdefaults files are ignored. The GMT class uses a fixed
3300 set of defaults, which may be altered via an argument to the constructor.
3301 If possible, GMT is run in 'isolation mode', which was introduced with GMT
3302 version 4.2.2, by setting `GMT_TMPDIR` to the instance's temporary
3303 directory. With earlier versions of GMT, problems may arise with parallel
3304 execution of more than one GMT instance.
3306 Each instance of the GMT class may pick a specific version of GMT which
3307 shall be used, so that, if multiple versions of GMT are installed on the
3308 system, different versions of GMT can be used simultaneously such that
3309 backward compatibility of the scripts can be maintained.
3311 '''
3313 def __init__(
3314 self,
3315 config=None,
3316 kontinue=None,
3317 version='newest',
3318 config_papersize=None,
3319 eps_mode=False):
3321 self.installation = get_gmt_installation(version)
3322 self.gmt_config = dict(self.installation['defaults'])
3323 self.eps_mode = eps_mode
3324 self._shutil = shutil
3326 if config:
3327 self.gmt_config.update(config)
3329 if config_papersize:
3330 if not isinstance(config_papersize, str):
3331 config_papersize = 'Custom_%ix%i' % (
3332 int(config_papersize[0]), int(config_papersize[1]))
3334 if self.is_gmt5():
3335 self.gmt_config['PS_MEDIA'] = config_papersize
3336 else:
3337 self.gmt_config['PAPER_MEDIA'] = config_papersize
3339 self.tempdir = tempfile.mkdtemp("", "gmtpy-")
3340 self.gmt_config_filename = pjoin(self.tempdir, 'gmt.conf')
3341 self.gen_gmt_config_file(self.gmt_config_filename, self.gmt_config)
3343 if kontinue is not None:
3344 self.load_unfinished(kontinue)
3345 self.needstart = False
3346 else:
3347 self.output = BytesIO()
3348 self.needstart = True
3350 self.finished = False
3352 self.environ = os.environ.copy()
3353 self.environ['GMTHOME'] = self.installation.get('home', '')
3354 # GMT isolation mode: works only properly with GMT version >= 4.2.2
3355 self.environ['GMT_TMPDIR'] = self.tempdir
3357 self.layout = None
3358 self.command_log = []
3359 self.keep_temp_dir = False
3361 def is_gmt5(self):
3362 return self.installation['version'][0] == '5'
3364 def get_version(self):
3365 return self.installation['version']
3367 def get_config(self, key):
3368 return self.gmt_config[key]
3370 def to_points(self, string):
3371 if not string:
3372 return 0
3374 unit = string[-1]
3375 if unit in _units:
3376 return float(string[:-1])/_units[unit]
3377 else:
3378 default_unit = measure_unit(self.gmt_config).lower()[0]
3379 return float(string)/_units[default_unit]
3381 def label_font_size(self):
3382 if self.is_gmt5():
3383 return self.to_points(self.gmt_config['FONT_LABEL'].split(',')[0])
3384 else:
3385 return self.to_points(self.gmt_config['LABEL_FONT_SIZE'])
3387 def label_font(self):
3388 if self.is_gmt5():
3389 return font_tab_rev(self.gmt_config['FONT_LABEL'].split(',')[1])
3390 else:
3391 return self.gmt_config['LABEL_FONT']
3393 def gen_gmt_config_file(self, config_filename, config):
3394 f = open(config_filename, 'wb')
3395 f.write(
3396 ('#\n# GMT %s Defaults file\n'
3397 % self.installation['version']).encode('ascii'))
3399 for k, v in config.items():
3400 f.write(('%s = %s\n' % (k, v)).encode('ascii'))
3401 f.close()
3403 def __del__(self):
3404 if not self.keep_temp_dir:
3405 self._shutil.rmtree(self.tempdir)
3407 def _gmtcommand(self, command, *addargs, **kwargs):
3409 '''
3410 Execute arbitrary GMT command.
3412 See docstring in __getattr__ for details.
3413 '''
3415 in_stream = kwargs.pop('in_stream', None)
3416 in_filename = kwargs.pop('in_filename', None)
3417 in_string = kwargs.pop('in_string', None)
3418 in_columns = kwargs.pop('in_columns', None)
3419 in_rows = kwargs.pop('in_rows', None)
3420 out_stream = kwargs.pop('out_stream', None)
3421 out_filename = kwargs.pop('out_filename', None)
3422 out_discard = kwargs.pop('out_discard', None)
3423 finish = kwargs.pop('finish', False)
3424 suppressdefaults = kwargs.pop('suppress_defaults', False)
3425 config_override = kwargs.pop('config', None)
3427 assert(not self.finished)
3429 # check for mutual exclusiveness on input and output possibilities
3430 assert(1 >= len(
3431 [x for x in [
3432 in_stream, in_filename, in_string, in_columns, in_rows]
3433 if x is not None]))
3434 assert(1 >= len([x for x in [out_stream, out_filename, out_discard]
3435 if x is not None]))
3437 options = []
3439 gmt_config = self.gmt_config
3440 if not self.is_gmt5():
3441 gmt_config_filename = self.gmt_config_filename
3442 if config_override:
3443 gmt_config = self.gmt_config.copy()
3444 gmt_config.update(config_override)
3445 gmt_config_override_filename = pjoin(
3446 self.tempdir, 'gmtdefaults_override')
3447 self.gen_gmt_config_file(
3448 gmt_config_override_filename, gmt_config)
3449 gmt_config_filename = gmt_config_override_filename
3451 else: # gmt5 needs override variables as --VAR=value
3452 if config_override:
3453 for k, v in config_override.items():
3454 options.append('--%s=%s' % (k, v))
3456 if out_discard:
3457 out_filename = '/dev/null'
3459 out_mustclose = False
3460 if out_filename is not None:
3461 out_mustclose = True
3462 out_stream = open(out_filename, 'wb')
3464 if in_filename is not None:
3465 in_stream = open(in_filename, 'rb')
3467 if in_string is not None:
3468 in_stream = BytesIO(in_string)
3470 encoding_gmt = gmt_config.get(
3471 'PS_CHAR_ENCODING',
3472 gmt_config.get('CHAR_ENCODING', 'ISOLatin1+'))
3474 encoding = encoding_gmt_to_python[encoding_gmt.lower()]
3476 if in_columns is not None or in_rows is not None:
3477 in_stream = LineStreamChopper(TableLiner(in_columns=in_columns,
3478 in_rows=in_rows,
3479 encoding=encoding))
3481 # convert option arguments to strings
3482 for k, v in kwargs.items():
3483 if len(k) > 1:
3484 raise GmtPyError('Found illegal keyword argument "%s" '
3485 'while preparing options for command "%s"'
3486 % (k, command))
3488 if type(v) is bool:
3489 if v:
3490 options.append('-%s' % k)
3491 elif type(v) is tuple or type(v) is list:
3492 options.append('-%s' % k + '/'.join([str(x) for x in v]))
3493 else:
3494 options.append('-%s%s' % (k, str(v)))
3496 # if not redirecting to an external sink, handle -K -O
3497 if out_stream is None:
3498 if not finish:
3499 options.append('-K')
3500 else:
3501 self.finished = True
3503 if not self.needstart:
3504 options.append('-O')
3505 else:
3506 self.needstart = False
3508 out_stream = self.output
3510 # run the command
3511 if self.is_gmt5():
3512 args = [pjoin(self.installation['bin'], 'gmt'), command]
3513 else:
3514 args = [pjoin(self.installation['bin'], command)]
3516 if not os.path.isfile(args[0]):
3517 raise OSError('No such file: %s' % args[0])
3518 args.extend(options)
3519 args.extend(addargs)
3520 if not self.is_gmt5() and not suppressdefaults:
3521 # does not seem to work with GMT 5 (and should not be necessary
3522 args.append('+'+gmt_config_filename)
3524 bs = 2048
3525 p = subprocess.Popen(args, stdin=subprocess.PIPE,
3526 stdout=subprocess.PIPE, bufsize=bs,
3527 env=self.environ)
3528 while True:
3529 cr, cw, cx = select([p.stdout], [p.stdin], [])
3530 if cr:
3531 out_stream.write(p.stdout.read(bs))
3532 if cw:
3533 if in_stream is not None:
3534 data = in_stream.read(bs)
3535 if len(data) == 0:
3536 break
3537 p.stdin.write(data)
3538 else:
3539 break
3540 if not cr and not cw:
3541 break
3543 p.stdin.close()
3545 while True:
3546 data = p.stdout.read(bs)
3547 if len(data) == 0:
3548 break
3549 out_stream.write(data)
3551 p.stdout.close()
3553 retcode = p.wait()
3555 if in_stream is not None:
3556 in_stream.close()
3558 if out_mustclose:
3559 out_stream.close()
3561 if retcode != 0:
3562 self.keep_temp_dir = True
3563 raise GMTError('Command %s returned an error. '
3564 'While executing command:\n%s'
3565 % (command, escape_shell_args(args)))
3567 self.command_log.append(args)
3569 def __getattr__(self, command):
3571 '''
3572 Maps to call self._gmtcommand(command, \\*addargs, \\*\\*kwargs).
3574 Execute arbitrary GMT command.
3576 Run a GMT command and by default append its postscript output to the
3577 output file maintained by the GMT instance on which this method is
3578 called.
3580 Except for a few keyword arguments listed below, any ``kwargs`` and
3581 ``addargs`` are converted into command line options and arguments and
3582 passed to the GMT command. Numbers in keyword arguments are converted
3583 into strings. E.g. ``S=10`` is translated into ``'-S10'``. Tuples of
3584 numbers or strings are converted into strings where the elements of the
3585 tuples are separated by slashes '/'. E.g. ``R=(10, 10, 20, 20)`` is
3586 translated into ``'-R10/10/20/20'``. Options with a boolean argument
3587 are only appended to the GMT command, if their values are True.
3589 If no output redirection is in effect, the -K and -O options are
3590 handled by gmtpy and thus should not be specified. Use
3591 ``out_discard=True`` if you don't want -K or -O beeing added, but are
3592 not interested in the output.
3594 The standard input of the GMT process is fed by data selected with one
3595 of the following ``in_*`` keyword arguments:
3597 =============== =======================================================
3598 ``in_stream`` Data is read from an open file like object.
3599 ``in_filename`` Data is read from the given file.
3600 ``in_string`` String content is dumped to the process.
3601 ``in_columns`` A 2D nested iterable whose elements can be accessed as
3602 ``in_columns[icolumn][irow]`` is converted into an
3603 ascii
3604 table, which is fed to the process.
3605 ``in_rows`` A 2D nested iterable whos elements can be accessed as
3606 ``in_rows[irow][icolumn]`` is converted into an ascii
3607 table, which is fed to the process.
3608 =============== =======================================================
3610 The standard output of the GMT process may be redirected by one of the
3611 following options:
3613 ================= =====================================================
3614 ``out_stream`` Output is fed to an open file like object.
3615 ``out_filename`` Output is dumped to the given file.
3616 ``out_discard`` If True, output is dumped to :file:`/dev/null`.
3617 ================= =====================================================
3619 Additional keyword arguments:
3621 ===================== =================================================
3622 ``config`` Dict with GMT defaults which override the
3623 currently active set of defaults exclusively
3624 during this call.
3625 ``finish`` If True, the postscript file, which is maintained
3626 by the GMT instance is finished, and no further
3627 plotting is allowed.
3628 ``suppress_defaults`` Suppress appending of the ``'+gmtdefaults'``
3629 option to the command.
3630 ===================== =================================================
3632 '''
3634 def f(*args, **kwargs):
3635 return self._gmtcommand(command, *args, **kwargs)
3636 return f
3638 def tempfilename(self, name=None):
3639 '''
3640 Get filename for temporary file in the private temp directory.
3642 If no ``name`` argument is given, a random name is picked. If
3643 ``name`` is given, returns a path ending in that ``name``.
3644 '''
3646 if not name:
3647 name = ''.join(
3648 [random.choice('abcdefghijklmnopqrstuvwxyz')
3649 for i in range(10)])
3651 fn = pjoin(self.tempdir, name)
3652 return fn
3654 def tempfile(self, name=None):
3655 '''
3656 Create and open a file in the private temp directory.
3657 '''
3659 fn = self.tempfilename(name)
3660 f = open(fn, 'wb')
3661 return f, fn
3663 def save_unfinished(self, filename):
3664 out = open(filename, 'wb')
3665 out.write(self.output.getvalue())
3666 out.close()
3668 def load_unfinished(self, filename):
3669 self.output = BytesIO()
3670 self.finished = False
3671 inp = open(filename, 'rb')
3672 self.output.write(inp.read())
3673 inp.close()
3675 def dump(self, ident):
3676 filename = self.tempfilename('breakpoint-%s' % ident)
3677 self.save_unfinished(filename)
3679 def load(self, ident):
3680 filename = self.tempfilename('breakpoint-%s' % ident)
3681 self.load_unfinished(filename)
3683 def save(self, filename=None, bbox=None, resolution=150, oversample=2.,
3684 width=None, height=None, size=None, crop_eps_mode=False,
3685 psconvert=False):
3687 '''
3688 Finish and save figure as PDF, PS or PPM file.
3690 If filename ends with ``'.pdf'`` a PDF file is created by piping the
3691 GMT output through :program:`gmtpy-epstopdf`.
3693 If filename ends with ``'.png'`` a PNG file is created by running
3694 :program:`gmtpy-epstopdf`, :program:`pdftocairo` and
3695 :program:`convert`. ``resolution`` specifies the resolution in DPI for
3696 raster file formats. Rasterization is done at a higher resolution if
3697 ``oversample`` is set to a value higher than one. The output image size
3698 can also be controlled by setting ``width``, ``height`` or ``size``
3699 instead of ``resolution``. When ``size`` is given, the image is scaled
3700 so that ``max(width, height) == size``.
3702 The bounding box is set according to the values given in ``bbox``.
3703 '''
3705 if not self.finished:
3706 self.psxy(R=True, J=True, finish=True)
3708 if filename:
3709 tempfn = pjoin(self.tempdir, 'incomplete')
3710 out = open(tempfn, 'wb')
3711 else:
3712 out = sys.stdout
3714 if bbox and not self.is_gmt5():
3715 out.write(replace_bbox(bbox, self.output.getvalue()))
3716 else:
3717 out.write(self.output.getvalue())
3719 if filename:
3720 out.close()
3722 if filename.endswith('.ps') or (
3723 not self.is_gmt5() and filename.endswith('.eps')):
3725 shutil.move(tempfn, filename)
3726 return
3728 if self.is_gmt5():
3729 if crop_eps_mode:
3730 addarg = ['-A0']
3731 else:
3732 addarg = []
3734 subprocess.call(
3735 [pjoin(self.installation['bin'], 'gmt'), 'psconvert',
3736 '-Te', '-F%s' % tempfn + '.eps', tempfn, ] + addarg)
3738 if bbox:
3739 with open(tempfn + '.eps', 'rb') as fin:
3740 with open(tempfn + '-fixbb.eps', 'wb') as fout:
3741 replace_bbox(bbox, fin, fout)
3743 shutil.move(tempfn + '-fixbb.eps', tempfn + '.eps')
3745 else:
3746 shutil.move(tempfn, tempfn + '.eps')
3748 if filename.endswith('.eps'):
3749 shutil.move(tempfn + '.eps', filename)
3750 return
3752 elif filename.endswith('.pdf'):
3753 if psconvert:
3754 gmt_bin = pjoin(self.installation['bin'], 'gmt')
3755 subprocess.call([gmt_bin, 'psconvert', tempfn, '-Tf',
3756 '-F' + filename])
3757 else:
3758 subprocess.call(['gmtpy-epstopdf', '--res=%i' % resolution,
3759 '--outfile=' + filename, tempfn + '.eps'])
3760 else:
3761 subprocess.call([
3762 'gmtpy-epstopdf',
3763 '--res=%i' % (resolution * oversample),
3764 '--outfile=' + tempfn + '.pdf', tempfn + '.eps'])
3766 convert_graph(
3767 tempfn + '.pdf', filename,
3768 resolution=resolution, oversample=oversample,
3769 size=size, width=width, height=height)
3771 def bbox(self):
3772 return get_bbox(self.output.getvalue())
3774 def get_command_log(self):
3775 '''
3776 Get the command log.
3777 '''
3779 return self.command_log
3781 def __str__(self):
3782 s = ''
3783 for com in self.command_log:
3784 s += com[0] + "\n " + "\n ".join(com[1:]) + "\n\n"
3785 return s
3787 def page_size_points(self):
3788 '''
3789 Try to get paper size of output postscript file in points.
3790 '''
3792 pm = paper_media(self.gmt_config).lower()
3793 if pm.endswith('+') or pm.endswith('-'):
3794 pm = pm[:-1]
3796 orient = page_orientation(self.gmt_config).lower()
3798 if pm in all_paper_sizes():
3800 if orient == 'portrait':
3801 return get_paper_size(pm)
3802 else:
3803 return get_paper_size(pm)[1], get_paper_size(pm)[0]
3805 m = re.match(r'custom_([0-9.]+)([cimp]?)x([0-9.]+)([cimp]?)', pm)
3806 if m:
3807 w, uw, h, uh = m.groups()
3808 w, h = float(w), float(h)
3809 if uw:
3810 w *= _units[uw]
3811 if uh:
3812 h *= _units[uh]
3813 if orient == 'portrait':
3814 return w, h
3815 else:
3816 return h, w
3818 return None, None
3820 def default_layout(self, with_palette=False):
3821 '''
3822 Get a default layout for the output page.
3824 One of three different layouts is choosen, depending on the
3825 `PAPER_MEDIA` setting in the GMT configuration dict.
3827 If `PAPER_MEDIA` ends with a ``'+'`` (EPS output is selected), a
3828 :py:class:`FrameLayout` is centered on the page, whose size is
3829 controlled by its center widget's size plus the margins of the
3830 :py:class:`FrameLayout`.
3832 If `PAPER_MEDIA` indicates, that a custom page size is wanted by
3833 starting with ``'Custom_'``, a :py:class:`FrameLayout` is used to fill
3834 the complete page. The center widget's size is then controlled by the
3835 page's size minus the margins of the :py:class:`FrameLayout`.
3837 In any other case, two FrameLayouts are nested, such that the outer
3838 layout attaches a 1 cm (printer) margin around the complete page, and
3839 the inner FrameLayout's center widget takes up as much space as
3840 possible under the constraint, that an aspect ratio of 1/golden_ratio
3841 is preserved.
3843 In any case, a reference to the innermost :py:class:`FrameLayout`
3844 instance is returned. The top-level layout can be accessed by calling
3845 :py:meth:`Widget.get_parent` on the returned layout.
3846 '''
3848 if self.layout is None:
3849 w, h = self.page_size_points()
3851 if w is None or h is None:
3852 raise GmtPyError("Can't determine page size for layout")
3854 pm = paper_media(self.gmt_config).lower()
3856 if with_palette:
3857 palette_layout = GridLayout(3, 1)
3858 spacer = palette_layout.get_widget(1, 0)
3859 palette_widget = palette_layout.get_widget(2, 0)
3860 spacer.set_horizontal(0.5*cm)
3861 palette_widget.set_horizontal(0.5*cm)
3863 if pm.endswith('+') or self.eps_mode:
3864 outer = CenterLayout()
3865 outer.set_policy((w, h), (0., 0.))
3866 inner = FrameLayout()
3867 outer.set_widget(inner)
3868 if with_palette:
3869 inner.set_widget('center', palette_layout)
3870 widget = palette_layout
3871 else:
3872 widget = inner.get_widget('center')
3873 widget.set_policy((w/golden_ratio, 0.), (0., 0.),
3874 aspect=1./golden_ratio)
3875 mw = 3.0*cm
3876 inner.set_fixed_margins(
3877 mw, mw, mw/golden_ratio, mw/golden_ratio)
3878 self.layout = inner
3880 elif pm.startswith('custom_'):
3881 layout = FrameLayout()
3882 layout.set_policy((w, h), (0., 0.))
3883 mw = 3.0*cm
3884 layout.set_min_margins(
3885 mw, mw, mw/golden_ratio, mw/golden_ratio)
3886 if with_palette:
3887 layout.set_widget('center', palette_layout)
3888 self.layout = layout
3889 else:
3890 outer = FrameLayout()
3891 outer.set_policy((w, h), (0., 0.))
3892 outer.set_fixed_margins(1.*cm, 1.*cm, 1.*cm, 1.*cm)
3894 inner = FrameLayout()
3895 outer.set_widget('center', inner)
3896 mw = 3.0*cm
3897 inner.set_min_margins(mw, mw, mw/golden_ratio, mw/golden_ratio)
3898 if with_palette:
3899 inner.set_widget('center', palette_layout)
3900 widget = palette_layout
3901 else:
3902 widget = inner.get_widget('center')
3904 widget.set_aspect(1./golden_ratio)
3906 self.layout = inner
3908 return self.layout
3910 def draw_layout(self, layout):
3911 '''
3912 Use psxy to draw layout; for debugging
3913 '''
3915 # corners = layout.get_corners(descend=True)
3916 rects = num.array(layout.get_sizes(), dtype=float)
3917 rects_wid = rects[:, 0, 0]
3918 rects_hei = rects[:, 0, 1]
3919 rects_center_x = rects[:, 1, 0] + rects_wid*0.5
3920 rects_center_y = rects[:, 1, 1] + rects_hei*0.5
3921 nrects = len(rects)
3922 prects = (rects_center_x, rects_center_y, num.arange(nrects),
3923 num.zeros(nrects), rects_hei, rects_wid)
3925 # points = num.array(corners, dtype=float)
3927 cptfile = self.tempfilename() + '.cpt'
3928 self.makecpt(
3929 C='ocean',
3930 T='%g/%g/%g' % (-nrects, nrects, 1),
3931 Z=True,
3932 out_filename=cptfile, suppress_defaults=True)
3934 bb = layout.bbox()
3935 self.psxy(
3936 in_columns=prects,
3937 C=cptfile,
3938 W='1p',
3939 S='J',
3940 R=(bb[0], bb[2], bb[1], bb[3]),
3941 *layout.XYJ())
3944def simpleconf_to_ax(conf, axname):
3945 c = {}
3946 x = axname
3947 for x in ('', axname):
3948 for k in ('label', 'unit', 'scaled_unit', 'scaled_unit_factor',
3949 'space', 'mode', 'approx_ticks', 'limits', 'masking', 'inc',
3950 'snap'):
3952 if x+k in conf:
3953 c[k] = conf[x+k]
3955 return Ax(**c)
3958class DensityPlotDef(object):
3959 def __init__(self, data, cpt='ocean', tension=0.7, size=(640, 480),
3960 contour=False, method='surface', zscaler=None, **extra):
3961 self.data = data
3962 self.cpt = cpt
3963 self.tension = tension
3964 self.size = size
3965 self.contour = contour
3966 self.method = method
3967 self.zscaler = zscaler
3968 self.extra = extra
3971class TextDef(object):
3972 def __init__(
3973 self,
3974 data,
3975 size=9,
3976 justify='MC',
3977 fontno=0,
3978 offset=(0, 0),
3979 color='black'):
3981 self.data = data
3982 self.size = size
3983 self.justify = justify
3984 self.fontno = fontno
3985 self.offset = offset
3986 self.color = color
3989class Simple(object):
3990 def __init__(self, gmtconfig=None, gmtversion='newest', **simple_config):
3991 self.data = []
3992 self.symbols = []
3993 self.config = copy.deepcopy(simple_config)
3994 self.gmtconfig = gmtconfig
3995 self.density_plot_defs = []
3996 self.text_defs = []
3998 self.gmtversion = gmtversion
4000 self.data_x = []
4001 self.symbols_x = []
4003 self.data_y = []
4004 self.symbols_y = []
4006 self.default_config = {}
4007 self.set_defaults(width=15.*cm,
4008 height=15.*cm / golden_ratio,
4009 margins=(2.*cm, 2.*cm, 2.*cm, 2.*cm),
4010 with_palette=False,
4011 palette_offset=0.5*cm,
4012 palette_width=None,
4013 palette_height=None,
4014 zlabeloffset=2*cm,
4015 draw_layout=False)
4017 self.setup_defaults()
4018 self.fixate_widget_aspect = False
4020 def setup_defaults(self):
4021 pass
4023 def set_defaults(self, **kwargs):
4024 self.default_config.update(kwargs)
4026 def plot(self, data, symbol=''):
4027 self.data.append(data)
4028 self.symbols.append(symbol)
4030 def density_plot(self, data, **kwargs):
4031 dpd = DensityPlotDef(data, **kwargs)
4032 self.density_plot_defs.append(dpd)
4034 def text(self, data, **kwargs):
4035 dpd = TextDef(data, **kwargs)
4036 self.text_defs.append(dpd)
4038 def plot_x(self, data, symbol=''):
4039 self.data_x.append(data)
4040 self.symbols_x.append(symbol)
4042 def plot_y(self, data, symbol=''):
4043 self.data_y.append(data)
4044 self.symbols_y.append(symbol)
4046 def set(self, **kwargs):
4047 self.config.update(kwargs)
4049 def setup_base(self, conf):
4050 w = conf.pop('width')
4051 h = conf.pop('height')
4052 margins = conf.pop('margins')
4054 gmtconfig = {}
4055 if self.gmtconfig is not None:
4056 gmtconfig.update(self.gmtconfig)
4058 gmt = GMT(
4059 version=self.gmtversion,
4060 config=gmtconfig,
4061 config_papersize='Custom_%ix%i' % (w, h))
4063 layout = gmt.default_layout(with_palette=conf['with_palette'])
4064 layout.set_min_margins(*margins)
4065 if conf['with_palette']:
4066 widget = layout.get_widget().get_widget(0, 0)
4067 spacer = layout.get_widget().get_widget(1, 0)
4068 spacer.set_horizontal(conf['palette_offset'])
4069 palette_widget = layout.get_widget().get_widget(2, 0)
4070 if conf['palette_width'] is not None:
4071 palette_widget.set_horizontal(conf['palette_width'])
4072 if conf['palette_height'] is not None:
4073 palette_widget.set_vertical(conf['palette_height'])
4074 widget.set_vertical(h-margins[2]-margins[3]-0.03*cm)
4075 return gmt, layout, widget, palette_widget
4076 else:
4077 widget = layout.get_widget()
4078 return gmt, layout, widget, None
4080 def setup_projection(self, widget, scaler, conf):
4081 pass
4083 def setup_scaling(self, conf):
4084 ndims = 2
4085 if self.density_plot_defs:
4086 ndims = 3
4088 axes = [simpleconf_to_ax(conf, x) for x in 'xyz'[:ndims]]
4090 data_all = []
4091 data_all.extend(self.data)
4092 for dsd in self.density_plot_defs:
4093 if dsd.zscaler is None:
4094 data_all.append(dsd.data)
4095 else:
4096 data_all.append(dsd.data[:2])
4097 data_chopped = [ds[:ndims] for ds in data_all]
4099 scaler = ScaleGuru(data_chopped, axes=axes[:ndims])
4101 self.setup_scaling_plus(scaler, axes[:ndims])
4103 return scaler
4105 def setup_scaling_plus(self, scaler, axes):
4106 pass
4108 def setup_scaling_extra(self, scaler, conf):
4110 scaler_x = scaler.copy()
4111 scaler_x.data_ranges[1] = (0., 1.)
4112 scaler_x.axes[1].mode = 'off'
4114 scaler_y = scaler.copy()
4115 scaler_y.data_ranges[0] = (0., 1.)
4116 scaler_y.axes[0].mode = 'off'
4118 return scaler_x, scaler_y
4120 def draw_density(self, gmt, widget, scaler):
4122 R = scaler.R()
4123 # par = scaler.get_params()
4124 rxyj = R + widget.XYJ()
4125 innerticks = False
4126 for dpd in self.density_plot_defs:
4128 fn_cpt = gmt.tempfilename() + '.cpt'
4130 if dpd.zscaler is not None:
4131 s = dpd.zscaler
4132 else:
4133 s = scaler
4135 gmt.makecpt(C=dpd.cpt, out_filename=fn_cpt, *s.T())
4137 fn_grid = gmt.tempfilename()
4139 fn_mean = gmt.tempfilename()
4141 if dpd.method in ('surface', 'triangulate'):
4142 gmt.blockmean(in_columns=dpd.data,
4143 I='%i+/%i+' % dpd.size, # noqa
4144 out_filename=fn_mean, *R)
4146 if dpd.method == 'surface':
4147 gmt.surface(
4148 in_filename=fn_mean,
4149 T=dpd.tension,
4150 G=fn_grid,
4151 I='%i+/%i+' % dpd.size, # noqa
4152 out_discard=True,
4153 *R)
4155 if dpd.method == 'triangulate':
4156 gmt.triangulate(
4157 in_filename=fn_mean,
4158 G=fn_grid,
4159 I='%i+/%i+' % dpd.size, # noqa
4160 out_discard=True,
4161 V=True,
4162 *R)
4164 if gmt.is_gmt5():
4165 gmt.grdimage(fn_grid, C=fn_cpt, E='i', n='l', *rxyj)
4167 else:
4168 gmt.grdimage(fn_grid, C=fn_cpt, E='i', S='l', *rxyj)
4170 if dpd.contour:
4171 gmt.grdcontour(fn_grid, C=fn_cpt, W='0.5p,black', *rxyj)
4172 innerticks = '0.5p,black'
4174 os.remove(fn_grid)
4175 os.remove(fn_mean)
4177 if dpd.method == 'fillcontour':
4178 extra = dict(C=fn_cpt)
4179 extra.update(dpd.extra)
4180 gmt.pscontour(in_columns=dpd.data,
4181 I=True, *rxyj, **extra) # noqa
4183 if dpd.method == 'contour':
4184 extra = dict(W='0.5p,black', C=fn_cpt)
4185 extra.update(dpd.extra)
4186 gmt.pscontour(in_columns=dpd.data, *rxyj, **extra)
4188 return fn_cpt, innerticks
4190 def draw_basemap(self, gmt, widget, scaler):
4191 gmt.psbasemap(*(widget.JXY() + scaler.RB(ax_projection=True)))
4193 def draw(self, gmt, widget, scaler):
4194 rxyj = scaler.R() + widget.JXY()
4195 for dat, sym in zip(self.data, self.symbols):
4196 gmt.psxy(in_columns=dat, *(sym.split()+rxyj))
4198 def post_draw(self, gmt, widget, scaler):
4199 pass
4201 def pre_draw(self, gmt, widget, scaler):
4202 pass
4204 def draw_extra(self, gmt, widget, scaler_x, scaler_y):
4206 for dat, sym in zip(self.data_x, self.symbols_x):
4207 gmt.psxy(in_columns=dat,
4208 *(sym.split() + scaler_x.R() + widget.JXY()))
4210 for dat, sym in zip(self.data_y, self.symbols_y):
4211 gmt.psxy(in_columns=dat,
4212 *(sym.split() + scaler_y.R() + widget.JXY()))
4214 def draw_text(self, gmt, widget, scaler):
4216 rxyj = scaler.R() + widget.JXY()
4217 for td in self.text_defs:
4218 x, y = td.data[0:2]
4219 text = td.data[-1]
4220 size = td.size
4221 angle = 0
4222 fontno = td.fontno
4223 justify = td.justify
4224 color = td.color
4225 if gmt.is_gmt5():
4226 gmt.pstext(
4227 in_rows=[(x, y, text)],
4228 F='+f%gp,%s,%s+a%g+j%s' % (
4229 size, fontno, color, angle, justify),
4230 D='%gp/%gp' % td.offset, *rxyj)
4231 else:
4232 gmt.pstext(
4233 in_rows=[(x, y, size, angle, fontno, justify, text)],
4234 D='%gp/%gp' % td.offset, *rxyj)
4236 def save(self, filename, resolution=150):
4238 conf = dict(self.default_config)
4239 conf.update(self.config)
4241 gmt, layout, widget, palette_widget = self.setup_base(conf)
4242 scaler = self.setup_scaling(conf)
4243 scaler_x, scaler_y = self.setup_scaling_extra(scaler, conf)
4245 self.setup_projection(widget, scaler, conf)
4246 if self.fixate_widget_aspect:
4247 aspect = aspect_for_projection(
4248 gmt.installation['version'], *(widget.J() + scaler.R()))
4250 widget.set_aspect(aspect)
4252 if conf['draw_layout']:
4253 gmt.draw_layout(layout)
4254 cptfile = None
4255 if self.density_plot_defs:
4256 cptfile, innerticks = self.draw_density(gmt, widget, scaler)
4257 self.pre_draw(gmt, widget, scaler)
4258 self.draw(gmt, widget, scaler)
4259 self.post_draw(gmt, widget, scaler)
4260 self.draw_extra(gmt, widget, scaler_x, scaler_y)
4261 self.draw_text(gmt, widget, scaler)
4262 self.draw_basemap(gmt, widget, scaler)
4264 if palette_widget and cptfile:
4265 nice_palette(gmt, palette_widget, scaler, cptfile,
4266 innerticks=innerticks,
4267 zlabeloffset=conf['zlabeloffset'])
4269 gmt.save(filename, resolution=resolution)
4272class LinLinPlot(Simple):
4273 pass
4276class LogLinPlot(Simple):
4278 def setup_defaults(self):
4279 self.set_defaults(xmode='min-max')
4281 def setup_projection(self, widget, scaler, conf):
4282 widget['J'] = '-JX%(width)gpl/%(height)gp'
4283 scaler['B'] = '-B2:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen'
4286class LinLogPlot(Simple):
4288 def setup_defaults(self):
4289 self.set_defaults(ymode='min-max')
4291 def setup_projection(self, widget, scaler, conf):
4292 widget['J'] = '-JX%(width)gp/%(height)gpl'
4293 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/2:%(ylabel)s:WSen'
4296class LogLogPlot(Simple):
4298 def setup_defaults(self):
4299 self.set_defaults(mode='min-max')
4301 def setup_projection(self, widget, scaler, conf):
4302 widget['J'] = '-JX%(width)gpl/%(height)gpl'
4303 scaler['B'] = '-B2:%(xlabel)s:/2:%(ylabel)s:WSen'
4306class AziDistPlot(Simple):
4308 def __init__(self, *args, **kwargs):
4309 Simple.__init__(self, *args, **kwargs)
4310 self.fixate_widget_aspect = True
4312 def setup_defaults(self):
4313 self.set_defaults(
4314 height=15.*cm,
4315 width=15.*cm,
4316 xmode='off',
4317 xlimits=(0., 360.),
4318 xinc=45.)
4320 def setup_projection(self, widget, scaler, conf):
4321 widget['J'] = '-JPa%(width)gp'
4323 def setup_scaling_plus(self, scaler, axes):
4324 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/%(yinc)g:%(ylabel)s:N'
4327class MPlot(Simple):
4329 def __init__(self, *args, **kwargs):
4330 Simple.__init__(self, *args, **kwargs)
4331 self.fixate_widget_aspect = True
4333 def setup_defaults(self):
4334 self.set_defaults(xmode='min-max', ymode='min-max')
4336 def setup_projection(self, widget, scaler, conf):
4337 par = scaler.get_params()
4338 lon0 = (par['xmin'] + par['xmax'])/2.
4339 lat0 = (par['ymin'] + par['ymax'])/2.
4340 sll = '%g/%g' % (lon0, lat0)
4341 widget['J'] = '-JM' + sll + '/%(width)gp'
4342 scaler['B'] = \
4343 '-B%(xinc)gg%(xinc)g:%(xlabel)s:/%(yinc)gg%(yinc)g:%(ylabel)s:WSen'
4346def nice_palette(gmt, widget, scaleguru, cptfile, zlabeloffset=0.8*inch,
4347 innerticks=True):
4349 par = scaleguru.get_params()
4350 par_ax = scaleguru.get_params(ax_projection=True)
4351 nz_palette = int(widget.height()/inch * 300)
4352 px = num.zeros(nz_palette*2)
4353 px[1::2] += 1
4354 pz = num.linspace(par['zmin'], par['zmax'], nz_palette).repeat(2)
4355 pdz = pz[2]-pz[0]
4356 palgrdfile = gmt.tempfilename()
4357 pal_r = (0, 1, par['zmin'], par['zmax'])
4358 pal_ax_r = (0, 1, par_ax['zmin'], par_ax['zmax'])
4359 gmt.xyz2grd(
4360 G=palgrdfile, R=pal_r,
4361 I=(1, pdz), in_columns=(px, pz, pz), # noqa
4362 out_discard=True)
4364 gmt.grdimage(palgrdfile, R=pal_r, C=cptfile, *widget.JXY())
4365 if isinstance(innerticks, str):
4366 tickpen = innerticks
4367 gmt.grdcontour(palgrdfile, W=tickpen, R=pal_r, C=cptfile,
4368 *widget.JXY())
4370 negpalwid = '%gp' % -widget.width()
4371 if not isinstance(innerticks, str) and innerticks:
4372 ticklen = negpalwid
4373 else:
4374 ticklen = '0p'
4376 TICK_LENGTH_PARAM = 'MAP_TICK_LENGTH' if gmt.is_gmt5() else 'TICK_LENGTH'
4377 gmt.psbasemap(
4378 R=pal_ax_r, B='4::/%(zinc)g::nsw' % par_ax,
4379 config={TICK_LENGTH_PARAM: ticklen},
4380 *widget.JXY())
4382 if innerticks:
4383 gmt.psbasemap(
4384 R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax,
4385 config={TICK_LENGTH_PARAM: '0p'},
4386 *widget.JXY())
4387 else:
4388 gmt.psbasemap(R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax, *widget.JXY())
4390 if par_ax['zlabel']:
4391 label_font = gmt.label_font()
4392 label_font_size = gmt.label_font_size()
4393 label_offset = zlabeloffset
4394 gmt.pstext(
4395 R=(0, 1, 0, 2), D="%gp/0p" % label_offset,
4396 N=True,
4397 in_rows=[(1, 1, label_font_size, -90, label_font, 'CB',
4398 par_ax['zlabel'])],
4399 *widget.JXY())