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['6.0.0'] = {'home': '/usr/share/gmt',
327# 'bin': '/usr/bin' }
329# ... or let GmtPy autodetect GMT via $PATH and $GMTHOME
332def key_version(a):
333 a = a.split('_')[0] # get rid of revision id
334 return [int(x) for x in a.split('.')]
337def newest_installed_gmt_version():
338 return sorted(_gmt_installations.keys(), key=key_version)[-1]
341def all_installed_gmt_versions():
342 return sorted(_gmt_installations.keys(), key=key_version)
345# To have consistent defaults, they are hardcoded here and should not be
346# changed.
348_gmt_defaults_by_version = {}
349_gmt_defaults_by_version['4.2.1'] = r'''
350#
351# GMT-SYSTEM 4.2.1 Defaults file
352#
353#-------- Plot Media Parameters -------------
354PAGE_COLOR = 255/255/255
355PAGE_ORIENTATION = portrait
356PAPER_MEDIA = a4+
357#-------- Basemap Annotation Parameters ------
358ANNOT_MIN_ANGLE = 20
359ANNOT_MIN_SPACING = 0
360ANNOT_FONT_PRIMARY = Helvetica
361ANNOT_FONT_SIZE = 12p
362ANNOT_OFFSET_PRIMARY = 0.075i
363ANNOT_FONT_SECONDARY = Helvetica
364ANNOT_FONT_SIZE_SECONDARY = 16p
365ANNOT_OFFSET_SECONDARY = 0.075i
366DEGREE_SYMBOL = ring
367HEADER_FONT = Helvetica
368HEADER_FONT_SIZE = 36p
369HEADER_OFFSET = 0.1875i
370LABEL_FONT = Helvetica
371LABEL_FONT_SIZE = 14p
372LABEL_OFFSET = 0.1125i
373OBLIQUE_ANNOTATION = 1
374PLOT_CLOCK_FORMAT = hh:mm:ss
375PLOT_DATE_FORMAT = yyyy-mm-dd
376PLOT_DEGREE_FORMAT = +ddd:mm:ss
377Y_AXIS_TYPE = hor_text
378#-------- Basemap Layout Parameters ---------
379BASEMAP_AXES = WESN
380BASEMAP_FRAME_RGB = 0/0/0
381BASEMAP_TYPE = plain
382FRAME_PEN = 1.25p
383FRAME_WIDTH = 0.075i
384GRID_CROSS_SIZE_PRIMARY = 0i
385GRID_CROSS_SIZE_SECONDARY = 0i
386GRID_PEN_PRIMARY = 0.25p
387GRID_PEN_SECONDARY = 0.5p
388MAP_SCALE_HEIGHT = 0.075i
389TICK_LENGTH = 0.075i
390POLAR_CAP = 85/90
391TICK_PEN = 0.5p
392X_AXIS_LENGTH = 9i
393Y_AXIS_LENGTH = 6i
394X_ORIGIN = 1i
395Y_ORIGIN = 1i
396UNIX_TIME = FALSE
397UNIX_TIME_POS = -0.75i/-0.75i
398#-------- Color System Parameters -----------
399COLOR_BACKGROUND = 0/0/0
400COLOR_FOREGROUND = 255/255/255
401COLOR_NAN = 128/128/128
402COLOR_IMAGE = adobe
403COLOR_MODEL = rgb
404HSV_MIN_SATURATION = 1
405HSV_MAX_SATURATION = 0.1
406HSV_MIN_VALUE = 0.3
407HSV_MAX_VALUE = 1
408#-------- PostScript Parameters -------------
409CHAR_ENCODING = ISOLatin1+
410DOTS_PR_INCH = 300
411N_COPIES = 1
412PS_COLOR = rgb
413PS_IMAGE_COMPRESS = none
414PS_IMAGE_FORMAT = ascii
415PS_LINE_CAP = round
416PS_LINE_JOIN = miter
417PS_MITER_LIMIT = 35
418PS_VERBOSE = FALSE
419GLOBAL_X_SCALE = 1
420GLOBAL_Y_SCALE = 1
421#-------- I/O Format Parameters -------------
422D_FORMAT = %lg
423FIELD_DELIMITER = tab
424GRIDFILE_SHORTHAND = FALSE
425GRID_FORMAT = nf
426INPUT_CLOCK_FORMAT = hh:mm:ss
427INPUT_DATE_FORMAT = yyyy-mm-dd
428IO_HEADER = FALSE
429N_HEADER_RECS = 1
430OUTPUT_CLOCK_FORMAT = hh:mm:ss
431OUTPUT_DATE_FORMAT = yyyy-mm-dd
432OUTPUT_DEGREE_FORMAT = +D
433XY_TOGGLE = FALSE
434#-------- Projection Parameters -------------
435ELLIPSOID = WGS-84
436MAP_SCALE_FACTOR = default
437MEASURE_UNIT = inch
438#-------- Calendar/Time Parameters ----------
439TIME_FORMAT_PRIMARY = full
440TIME_FORMAT_SECONDARY = full
441TIME_EPOCH = 2000-01-01T00:00:00
442TIME_IS_INTERVAL = OFF
443TIME_INTERVAL_FRACTION = 0.5
444TIME_LANGUAGE = us
445TIME_SYSTEM = other
446TIME_UNIT = d
447TIME_WEEK_START = Sunday
448Y2K_OFFSET_YEAR = 1950
449#-------- Miscellaneous Parameters ----------
450HISTORY = TRUE
451INTERPOLANT = akima
452LINE_STEP = 0.01i
453VECTOR_SHAPE = 0
454VERBOSE = FALSE'''
456_gmt_defaults_by_version['4.3.0'] = r'''
457#
458# GMT-SYSTEM 4.3.0 Defaults file
459#
460#-------- Plot Media Parameters -------------
461PAGE_COLOR = 255/255/255
462PAGE_ORIENTATION = portrait
463PAPER_MEDIA = a4+
464#-------- Basemap Annotation Parameters ------
465ANNOT_MIN_ANGLE = 20
466ANNOT_MIN_SPACING = 0
467ANNOT_FONT_PRIMARY = Helvetica
468ANNOT_FONT_SIZE_PRIMARY = 12p
469ANNOT_OFFSET_PRIMARY = 0.075i
470ANNOT_FONT_SECONDARY = Helvetica
471ANNOT_FONT_SIZE_SECONDARY = 16p
472ANNOT_OFFSET_SECONDARY = 0.075i
473DEGREE_SYMBOL = ring
474HEADER_FONT = Helvetica
475HEADER_FONT_SIZE = 36p
476HEADER_OFFSET = 0.1875i
477LABEL_FONT = Helvetica
478LABEL_FONT_SIZE = 14p
479LABEL_OFFSET = 0.1125i
480OBLIQUE_ANNOTATION = 1
481PLOT_CLOCK_FORMAT = hh:mm:ss
482PLOT_DATE_FORMAT = yyyy-mm-dd
483PLOT_DEGREE_FORMAT = +ddd:mm:ss
484Y_AXIS_TYPE = hor_text
485#-------- Basemap Layout Parameters ---------
486BASEMAP_AXES = WESN
487BASEMAP_FRAME_RGB = 0/0/0
488BASEMAP_TYPE = plain
489FRAME_PEN = 1.25p
490FRAME_WIDTH = 0.075i
491GRID_CROSS_SIZE_PRIMARY = 0i
492GRID_PEN_PRIMARY = 0.25p
493GRID_CROSS_SIZE_SECONDARY = 0i
494GRID_PEN_SECONDARY = 0.5p
495MAP_SCALE_HEIGHT = 0.075i
496POLAR_CAP = 85/90
497TICK_LENGTH = 0.075i
498TICK_PEN = 0.5p
499X_AXIS_LENGTH = 9i
500Y_AXIS_LENGTH = 6i
501X_ORIGIN = 1i
502Y_ORIGIN = 1i
503UNIX_TIME = FALSE
504UNIX_TIME_POS = BL/-0.75i/-0.75i
505UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
506#-------- Color System Parameters -----------
507COLOR_BACKGROUND = 0/0/0
508COLOR_FOREGROUND = 255/255/255
509COLOR_NAN = 128/128/128
510COLOR_IMAGE = adobe
511COLOR_MODEL = rgb
512HSV_MIN_SATURATION = 1
513HSV_MAX_SATURATION = 0.1
514HSV_MIN_VALUE = 0.3
515HSV_MAX_VALUE = 1
516#-------- PostScript Parameters -------------
517CHAR_ENCODING = ISOLatin1+
518DOTS_PR_INCH = 300
519N_COPIES = 1
520PS_COLOR = rgb
521PS_IMAGE_COMPRESS = none
522PS_IMAGE_FORMAT = ascii
523PS_LINE_CAP = round
524PS_LINE_JOIN = miter
525PS_MITER_LIMIT = 35
526PS_VERBOSE = FALSE
527GLOBAL_X_SCALE = 1
528GLOBAL_Y_SCALE = 1
529#-------- I/O Format Parameters -------------
530D_FORMAT = %lg
531FIELD_DELIMITER = tab
532GRIDFILE_SHORTHAND = FALSE
533GRID_FORMAT = nf
534INPUT_CLOCK_FORMAT = hh:mm:ss
535INPUT_DATE_FORMAT = yyyy-mm-dd
536IO_HEADER = FALSE
537N_HEADER_RECS = 1
538OUTPUT_CLOCK_FORMAT = hh:mm:ss
539OUTPUT_DATE_FORMAT = yyyy-mm-dd
540OUTPUT_DEGREE_FORMAT = +D
541XY_TOGGLE = FALSE
542#-------- Projection Parameters -------------
543ELLIPSOID = WGS-84
544MAP_SCALE_FACTOR = default
545MEASURE_UNIT = inch
546#-------- Calendar/Time Parameters ----------
547TIME_FORMAT_PRIMARY = full
548TIME_FORMAT_SECONDARY = full
549TIME_EPOCH = 2000-01-01T00:00:00
550TIME_IS_INTERVAL = OFF
551TIME_INTERVAL_FRACTION = 0.5
552TIME_LANGUAGE = us
553TIME_UNIT = d
554TIME_WEEK_START = Sunday
555Y2K_OFFSET_YEAR = 1950
556#-------- Miscellaneous Parameters ----------
557HISTORY = TRUE
558INTERPOLANT = akima
559LINE_STEP = 0.01i
560VECTOR_SHAPE = 0
561VERBOSE = FALSE'''
564_gmt_defaults_by_version['4.3.1'] = r'''
565#
566# GMT-SYSTEM 4.3.1 Defaults file
567#
568#-------- Plot Media Parameters -------------
569PAGE_COLOR = 255/255/255
570PAGE_ORIENTATION = portrait
571PAPER_MEDIA = a4+
572#-------- Basemap Annotation Parameters ------
573ANNOT_MIN_ANGLE = 20
574ANNOT_MIN_SPACING = 0
575ANNOT_FONT_PRIMARY = Helvetica
576ANNOT_FONT_SIZE_PRIMARY = 12p
577ANNOT_OFFSET_PRIMARY = 0.075i
578ANNOT_FONT_SECONDARY = Helvetica
579ANNOT_FONT_SIZE_SECONDARY = 16p
580ANNOT_OFFSET_SECONDARY = 0.075i
581DEGREE_SYMBOL = ring
582HEADER_FONT = Helvetica
583HEADER_FONT_SIZE = 36p
584HEADER_OFFSET = 0.1875i
585LABEL_FONT = Helvetica
586LABEL_FONT_SIZE = 14p
587LABEL_OFFSET = 0.1125i
588OBLIQUE_ANNOTATION = 1
589PLOT_CLOCK_FORMAT = hh:mm:ss
590PLOT_DATE_FORMAT = yyyy-mm-dd
591PLOT_DEGREE_FORMAT = +ddd:mm:ss
592Y_AXIS_TYPE = hor_text
593#-------- Basemap Layout Parameters ---------
594BASEMAP_AXES = WESN
595BASEMAP_FRAME_RGB = 0/0/0
596BASEMAP_TYPE = plain
597FRAME_PEN = 1.25p
598FRAME_WIDTH = 0.075i
599GRID_CROSS_SIZE_PRIMARY = 0i
600GRID_PEN_PRIMARY = 0.25p
601GRID_CROSS_SIZE_SECONDARY = 0i
602GRID_PEN_SECONDARY = 0.5p
603MAP_SCALE_HEIGHT = 0.075i
604POLAR_CAP = 85/90
605TICK_LENGTH = 0.075i
606TICK_PEN = 0.5p
607X_AXIS_LENGTH = 9i
608Y_AXIS_LENGTH = 6i
609X_ORIGIN = 1i
610Y_ORIGIN = 1i
611UNIX_TIME = FALSE
612UNIX_TIME_POS = BL/-0.75i/-0.75i
613UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
614#-------- Color System Parameters -----------
615COLOR_BACKGROUND = 0/0/0
616COLOR_FOREGROUND = 255/255/255
617COLOR_NAN = 128/128/128
618COLOR_IMAGE = adobe
619COLOR_MODEL = rgb
620HSV_MIN_SATURATION = 1
621HSV_MAX_SATURATION = 0.1
622HSV_MIN_VALUE = 0.3
623HSV_MAX_VALUE = 1
624#-------- PostScript Parameters -------------
625CHAR_ENCODING = ISOLatin1+
626DOTS_PR_INCH = 300
627N_COPIES = 1
628PS_COLOR = rgb
629PS_IMAGE_COMPRESS = none
630PS_IMAGE_FORMAT = ascii
631PS_LINE_CAP = round
632PS_LINE_JOIN = miter
633PS_MITER_LIMIT = 35
634PS_VERBOSE = FALSE
635GLOBAL_X_SCALE = 1
636GLOBAL_Y_SCALE = 1
637#-------- I/O Format Parameters -------------
638D_FORMAT = %lg
639FIELD_DELIMITER = tab
640GRIDFILE_SHORTHAND = FALSE
641GRID_FORMAT = nf
642INPUT_CLOCK_FORMAT = hh:mm:ss
643INPUT_DATE_FORMAT = yyyy-mm-dd
644IO_HEADER = FALSE
645N_HEADER_RECS = 1
646OUTPUT_CLOCK_FORMAT = hh:mm:ss
647OUTPUT_DATE_FORMAT = yyyy-mm-dd
648OUTPUT_DEGREE_FORMAT = +D
649XY_TOGGLE = FALSE
650#-------- Projection Parameters -------------
651ELLIPSOID = WGS-84
652MAP_SCALE_FACTOR = default
653MEASURE_UNIT = inch
654#-------- Calendar/Time Parameters ----------
655TIME_FORMAT_PRIMARY = full
656TIME_FORMAT_SECONDARY = full
657TIME_EPOCH = 2000-01-01T00:00:00
658TIME_IS_INTERVAL = OFF
659TIME_INTERVAL_FRACTION = 0.5
660TIME_LANGUAGE = us
661TIME_UNIT = d
662TIME_WEEK_START = Sunday
663Y2K_OFFSET_YEAR = 1950
664#-------- Miscellaneous Parameters ----------
665HISTORY = TRUE
666INTERPOLANT = akima
667LINE_STEP = 0.01i
668VECTOR_SHAPE = 0
669VERBOSE = FALSE'''
672_gmt_defaults_by_version['4.4.0'] = r'''
673#
674# GMT-SYSTEM 4.4.0 [64-bit] Defaults file
675#
676#-------- Plot Media Parameters -------------
677PAGE_COLOR = 255/255/255
678PAGE_ORIENTATION = portrait
679PAPER_MEDIA = a4+
680#-------- Basemap Annotation Parameters ------
681ANNOT_MIN_ANGLE = 20
682ANNOT_MIN_SPACING = 0
683ANNOT_FONT_PRIMARY = Helvetica
684ANNOT_FONT_SIZE_PRIMARY = 14p
685ANNOT_OFFSET_PRIMARY = 0.075i
686ANNOT_FONT_SECONDARY = Helvetica
687ANNOT_FONT_SIZE_SECONDARY = 16p
688ANNOT_OFFSET_SECONDARY = 0.075i
689DEGREE_SYMBOL = ring
690HEADER_FONT = Helvetica
691HEADER_FONT_SIZE = 36p
692HEADER_OFFSET = 0.1875i
693LABEL_FONT = Helvetica
694LABEL_FONT_SIZE = 14p
695LABEL_OFFSET = 0.1125i
696OBLIQUE_ANNOTATION = 1
697PLOT_CLOCK_FORMAT = hh:mm:ss
698PLOT_DATE_FORMAT = yyyy-mm-dd
699PLOT_DEGREE_FORMAT = +ddd:mm:ss
700Y_AXIS_TYPE = hor_text
701#-------- Basemap Layout Parameters ---------
702BASEMAP_AXES = WESN
703BASEMAP_FRAME_RGB = 0/0/0
704BASEMAP_TYPE = plain
705FRAME_PEN = 1.25p
706FRAME_WIDTH = 0.075i
707GRID_CROSS_SIZE_PRIMARY = 0i
708GRID_PEN_PRIMARY = 0.25p
709GRID_CROSS_SIZE_SECONDARY = 0i
710GRID_PEN_SECONDARY = 0.5p
711MAP_SCALE_HEIGHT = 0.075i
712POLAR_CAP = 85/90
713TICK_LENGTH = 0.075i
714TICK_PEN = 0.5p
715X_AXIS_LENGTH = 9i
716Y_AXIS_LENGTH = 6i
717X_ORIGIN = 1i
718Y_ORIGIN = 1i
719UNIX_TIME = FALSE
720UNIX_TIME_POS = BL/-0.75i/-0.75i
721UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
722#-------- Color System Parameters -----------
723COLOR_BACKGROUND = 0/0/0
724COLOR_FOREGROUND = 255/255/255
725COLOR_NAN = 128/128/128
726COLOR_IMAGE = adobe
727COLOR_MODEL = rgb
728HSV_MIN_SATURATION = 1
729HSV_MAX_SATURATION = 0.1
730HSV_MIN_VALUE = 0.3
731HSV_MAX_VALUE = 1
732#-------- PostScript Parameters -------------
733CHAR_ENCODING = ISOLatin1+
734DOTS_PR_INCH = 300
735N_COPIES = 1
736PS_COLOR = rgb
737PS_IMAGE_COMPRESS = lzw
738PS_IMAGE_FORMAT = ascii
739PS_LINE_CAP = round
740PS_LINE_JOIN = miter
741PS_MITER_LIMIT = 35
742PS_VERBOSE = FALSE
743GLOBAL_X_SCALE = 1
744GLOBAL_Y_SCALE = 1
745#-------- I/O Format Parameters -------------
746D_FORMAT = %lg
747FIELD_DELIMITER = tab
748GRIDFILE_SHORTHAND = FALSE
749GRID_FORMAT = nf
750INPUT_CLOCK_FORMAT = hh:mm:ss
751INPUT_DATE_FORMAT = yyyy-mm-dd
752IO_HEADER = FALSE
753N_HEADER_RECS = 1
754OUTPUT_CLOCK_FORMAT = hh:mm:ss
755OUTPUT_DATE_FORMAT = yyyy-mm-dd
756OUTPUT_DEGREE_FORMAT = +D
757XY_TOGGLE = FALSE
758#-------- Projection Parameters -------------
759ELLIPSOID = WGS-84
760MAP_SCALE_FACTOR = default
761MEASURE_UNIT = inch
762#-------- Calendar/Time Parameters ----------
763TIME_FORMAT_PRIMARY = full
764TIME_FORMAT_SECONDARY = full
765TIME_EPOCH = 2000-01-01T00:00:00
766TIME_IS_INTERVAL = OFF
767TIME_INTERVAL_FRACTION = 0.5
768TIME_LANGUAGE = us
769TIME_UNIT = d
770TIME_WEEK_START = Sunday
771Y2K_OFFSET_YEAR = 1950
772#-------- Miscellaneous Parameters ----------
773HISTORY = TRUE
774INTERPOLANT = akima
775LINE_STEP = 0.01i
776VECTOR_SHAPE = 0
777VERBOSE = FALSE
778'''
780_gmt_defaults_by_version['4.5.2'] = r'''
781#
782# GMT-SYSTEM 4.5.2 [64-bit] Defaults file
783#
784#-------- Plot Media Parameters -------------
785PAGE_COLOR = white
786PAGE_ORIENTATION = portrait
787PAPER_MEDIA = a4+
788#-------- Basemap Annotation Parameters ------
789ANNOT_MIN_ANGLE = 20
790ANNOT_MIN_SPACING = 0
791ANNOT_FONT_PRIMARY = Helvetica
792ANNOT_FONT_SIZE_PRIMARY = 14p
793ANNOT_OFFSET_PRIMARY = 0.075i
794ANNOT_FONT_SECONDARY = Helvetica
795ANNOT_FONT_SIZE_SECONDARY = 16p
796ANNOT_OFFSET_SECONDARY = 0.075i
797DEGREE_SYMBOL = ring
798HEADER_FONT = Helvetica
799HEADER_FONT_SIZE = 36p
800HEADER_OFFSET = 0.1875i
801LABEL_FONT = Helvetica
802LABEL_FONT_SIZE = 14p
803LABEL_OFFSET = 0.1125i
804OBLIQUE_ANNOTATION = 1
805PLOT_CLOCK_FORMAT = hh:mm:ss
806PLOT_DATE_FORMAT = yyyy-mm-dd
807PLOT_DEGREE_FORMAT = +ddd:mm:ss
808Y_AXIS_TYPE = hor_text
809#-------- Basemap Layout Parameters ---------
810BASEMAP_AXES = WESN
811BASEMAP_FRAME_RGB = black
812BASEMAP_TYPE = plain
813FRAME_PEN = 1.25p
814FRAME_WIDTH = 0.075i
815GRID_CROSS_SIZE_PRIMARY = 0i
816GRID_PEN_PRIMARY = 0.25p
817GRID_CROSS_SIZE_SECONDARY = 0i
818GRID_PEN_SECONDARY = 0.5p
819MAP_SCALE_HEIGHT = 0.075i
820POLAR_CAP = 85/90
821TICK_LENGTH = 0.075i
822TICK_PEN = 0.5p
823X_AXIS_LENGTH = 9i
824Y_AXIS_LENGTH = 6i
825X_ORIGIN = 1i
826Y_ORIGIN = 1i
827UNIX_TIME = FALSE
828UNIX_TIME_POS = BL/-0.75i/-0.75i
829UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
830#-------- Color System Parameters -----------
831COLOR_BACKGROUND = black
832COLOR_FOREGROUND = white
833COLOR_NAN = 128
834COLOR_IMAGE = adobe
835COLOR_MODEL = rgb
836HSV_MIN_SATURATION = 1
837HSV_MAX_SATURATION = 0.1
838HSV_MIN_VALUE = 0.3
839HSV_MAX_VALUE = 1
840#-------- PostScript Parameters -------------
841CHAR_ENCODING = ISOLatin1+
842DOTS_PR_INCH = 300
843GLOBAL_X_SCALE = 1
844GLOBAL_Y_SCALE = 1
845N_COPIES = 1
846PS_COLOR = rgb
847PS_IMAGE_COMPRESS = lzw
848PS_IMAGE_FORMAT = ascii
849PS_LINE_CAP = round
850PS_LINE_JOIN = miter
851PS_MITER_LIMIT = 35
852PS_VERBOSE = FALSE
853TRANSPARENCY = 0
854#-------- I/O Format Parameters -------------
855D_FORMAT = %.12lg
856FIELD_DELIMITER = tab
857GRIDFILE_FORMAT = nf
858GRIDFILE_SHORTHAND = FALSE
859INPUT_CLOCK_FORMAT = hh:mm:ss
860INPUT_DATE_FORMAT = yyyy-mm-dd
861IO_HEADER = FALSE
862N_HEADER_RECS = 1
863NAN_RECORDS = pass
864OUTPUT_CLOCK_FORMAT = hh:mm:ss
865OUTPUT_DATE_FORMAT = yyyy-mm-dd
866OUTPUT_DEGREE_FORMAT = D
867XY_TOGGLE = FALSE
868#-------- Projection Parameters -------------
869ELLIPSOID = WGS-84
870MAP_SCALE_FACTOR = default
871MEASURE_UNIT = inch
872#-------- Calendar/Time Parameters ----------
873TIME_FORMAT_PRIMARY = full
874TIME_FORMAT_SECONDARY = full
875TIME_EPOCH = 2000-01-01T00:00:00
876TIME_IS_INTERVAL = OFF
877TIME_INTERVAL_FRACTION = 0.5
878TIME_LANGUAGE = us
879TIME_UNIT = d
880TIME_WEEK_START = Sunday
881Y2K_OFFSET_YEAR = 1950
882#-------- Miscellaneous Parameters ----------
883HISTORY = TRUE
884INTERPOLANT = akima
885LINE_STEP = 0.01i
886VECTOR_SHAPE = 0
887VERBOSE = FALSE
888'''
890_gmt_defaults_by_version['4.5.3'] = r'''
891#
892# GMT-SYSTEM 4.5.3 (CVS Jun 18 2010 10:56:07) [64-bit] Defaults file
893#
894#-------- Plot Media Parameters -------------
895PAGE_COLOR = white
896PAGE_ORIENTATION = portrait
897PAPER_MEDIA = a4+
898#-------- Basemap Annotation Parameters ------
899ANNOT_MIN_ANGLE = 20
900ANNOT_MIN_SPACING = 0
901ANNOT_FONT_PRIMARY = Helvetica
902ANNOT_FONT_SIZE_PRIMARY = 14p
903ANNOT_OFFSET_PRIMARY = 0.075i
904ANNOT_FONT_SECONDARY = Helvetica
905ANNOT_FONT_SIZE_SECONDARY = 16p
906ANNOT_OFFSET_SECONDARY = 0.075i
907DEGREE_SYMBOL = ring
908HEADER_FONT = Helvetica
909HEADER_FONT_SIZE = 36p
910HEADER_OFFSET = 0.1875i
911LABEL_FONT = Helvetica
912LABEL_FONT_SIZE = 14p
913LABEL_OFFSET = 0.1125i
914OBLIQUE_ANNOTATION = 1
915PLOT_CLOCK_FORMAT = hh:mm:ss
916PLOT_DATE_FORMAT = yyyy-mm-dd
917PLOT_DEGREE_FORMAT = +ddd:mm:ss
918Y_AXIS_TYPE = hor_text
919#-------- Basemap Layout Parameters ---------
920BASEMAP_AXES = WESN
921BASEMAP_FRAME_RGB = black
922BASEMAP_TYPE = plain
923FRAME_PEN = 1.25p
924FRAME_WIDTH = 0.075i
925GRID_CROSS_SIZE_PRIMARY = 0i
926GRID_PEN_PRIMARY = 0.25p
927GRID_CROSS_SIZE_SECONDARY = 0i
928GRID_PEN_SECONDARY = 0.5p
929MAP_SCALE_HEIGHT = 0.075i
930POLAR_CAP = 85/90
931TICK_LENGTH = 0.075i
932TICK_PEN = 0.5p
933X_AXIS_LENGTH = 9i
934Y_AXIS_LENGTH = 6i
935X_ORIGIN = 1i
936Y_ORIGIN = 1i
937UNIX_TIME = FALSE
938UNIX_TIME_POS = BL/-0.75i/-0.75i
939UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
940#-------- Color System Parameters -----------
941COLOR_BACKGROUND = black
942COLOR_FOREGROUND = white
943COLOR_NAN = 128
944COLOR_IMAGE = adobe
945COLOR_MODEL = rgb
946HSV_MIN_SATURATION = 1
947HSV_MAX_SATURATION = 0.1
948HSV_MIN_VALUE = 0.3
949HSV_MAX_VALUE = 1
950#-------- PostScript Parameters -------------
951CHAR_ENCODING = ISOLatin1+
952DOTS_PR_INCH = 300
953GLOBAL_X_SCALE = 1
954GLOBAL_Y_SCALE = 1
955N_COPIES = 1
956PS_COLOR = rgb
957PS_IMAGE_COMPRESS = lzw
958PS_IMAGE_FORMAT = ascii
959PS_LINE_CAP = round
960PS_LINE_JOIN = miter
961PS_MITER_LIMIT = 35
962PS_VERBOSE = FALSE
963TRANSPARENCY = 0
964#-------- I/O Format Parameters -------------
965D_FORMAT = %.12lg
966FIELD_DELIMITER = tab
967GRIDFILE_FORMAT = nf
968GRIDFILE_SHORTHAND = FALSE
969INPUT_CLOCK_FORMAT = hh:mm:ss
970INPUT_DATE_FORMAT = yyyy-mm-dd
971IO_HEADER = FALSE
972N_HEADER_RECS = 1
973NAN_RECORDS = pass
974OUTPUT_CLOCK_FORMAT = hh:mm:ss
975OUTPUT_DATE_FORMAT = yyyy-mm-dd
976OUTPUT_DEGREE_FORMAT = D
977XY_TOGGLE = FALSE
978#-------- Projection Parameters -------------
979ELLIPSOID = WGS-84
980MAP_SCALE_FACTOR = default
981MEASURE_UNIT = inch
982#-------- Calendar/Time Parameters ----------
983TIME_FORMAT_PRIMARY = full
984TIME_FORMAT_SECONDARY = full
985TIME_EPOCH = 2000-01-01T00:00:00
986TIME_IS_INTERVAL = OFF
987TIME_INTERVAL_FRACTION = 0.5
988TIME_LANGUAGE = us
989TIME_UNIT = d
990TIME_WEEK_START = Sunday
991Y2K_OFFSET_YEAR = 1950
992#-------- Miscellaneous Parameters ----------
993HISTORY = TRUE
994INTERPOLANT = akima
995LINE_STEP = 0.01i
996VECTOR_SHAPE = 0
997VERBOSE = FALSE
998'''
1000_gmt_defaults_by_version['5.1.2'] = r'''
1001#
1002# GMT 5.1.2 Defaults file
1003# vim:sw=8:ts=8:sts=8
1004# $Revision: 13836 $
1005# $LastChangedDate: 2014-12-20 03:45:42 -1000 (Sat, 20 Dec 2014) $
1006#
1007# COLOR Parameters
1008#
1009COLOR_BACKGROUND = black
1010COLOR_FOREGROUND = white
1011COLOR_NAN = 127.5
1012COLOR_MODEL = none
1013COLOR_HSV_MIN_S = 1
1014COLOR_HSV_MAX_S = 0.1
1015COLOR_HSV_MIN_V = 0.3
1016COLOR_HSV_MAX_V = 1
1017#
1018# DIR Parameters
1019#
1020DIR_DATA =
1021DIR_DCW =
1022DIR_GSHHG =
1023#
1024# FONT Parameters
1025#
1026FONT_ANNOT_PRIMARY = 14p,Helvetica,black
1027FONT_ANNOT_SECONDARY = 16p,Helvetica,black
1028FONT_LABEL = 14p,Helvetica,black
1029FONT_LOGO = 8p,Helvetica,black
1030FONT_TITLE = 24p,Helvetica,black
1031#
1032# FORMAT Parameters
1033#
1034FORMAT_CLOCK_IN = hh:mm:ss
1035FORMAT_CLOCK_OUT = hh:mm:ss
1036FORMAT_CLOCK_MAP = hh:mm:ss
1037FORMAT_DATE_IN = yyyy-mm-dd
1038FORMAT_DATE_OUT = yyyy-mm-dd
1039FORMAT_DATE_MAP = yyyy-mm-dd
1040FORMAT_GEO_OUT = D
1041FORMAT_GEO_MAP = ddd:mm:ss
1042FORMAT_FLOAT_OUT = %.12g
1043FORMAT_FLOAT_MAP = %.12g
1044FORMAT_TIME_PRIMARY_MAP = full
1045FORMAT_TIME_SECONDARY_MAP = full
1046FORMAT_TIME_STAMP = %Y %b %d %H:%M:%S
1047#
1048# GMT Miscellaneous Parameters
1049#
1050GMT_COMPATIBILITY = 4
1051GMT_CUSTOM_LIBS =
1052GMT_EXTRAPOLATE_VAL = NaN
1053GMT_FFT = auto
1054GMT_HISTORY = true
1055GMT_INTERPOLANT = akima
1056GMT_TRIANGULATE = Shewchuk
1057GMT_VERBOSE = compat
1058GMT_LANGUAGE = us
1059#
1060# I/O Parameters
1061#
1062IO_COL_SEPARATOR = tab
1063IO_GRIDFILE_FORMAT = nf
1064IO_GRIDFILE_SHORTHAND = false
1065IO_HEADER = false
1066IO_N_HEADER_RECS = 0
1067IO_NAN_RECORDS = pass
1068IO_NC4_CHUNK_SIZE = auto
1069IO_NC4_DEFLATION_LEVEL = 3
1070IO_LONLAT_TOGGLE = false
1071IO_SEGMENT_MARKER = >
1072#
1073# MAP Parameters
1074#
1075MAP_ANNOT_MIN_ANGLE = 20
1076MAP_ANNOT_MIN_SPACING = 0p
1077MAP_ANNOT_OBLIQUE = 1
1078MAP_ANNOT_OFFSET_PRIMARY = 0.075i
1079MAP_ANNOT_OFFSET_SECONDARY = 0.075i
1080MAP_ANNOT_ORTHO = we
1081MAP_DEFAULT_PEN = default,black
1082MAP_DEGREE_SYMBOL = ring
1083MAP_FRAME_AXES = WESNZ
1084MAP_FRAME_PEN = thicker,black
1085MAP_FRAME_TYPE = fancy
1086MAP_FRAME_WIDTH = 5p
1087MAP_GRID_CROSS_SIZE_PRIMARY = 0p
1088MAP_GRID_CROSS_SIZE_SECONDARY = 0p
1089MAP_GRID_PEN_PRIMARY = default,black
1090MAP_GRID_PEN_SECONDARY = thinner,black
1091MAP_LABEL_OFFSET = 0.1944i
1092MAP_LINE_STEP = 0.75p
1093MAP_LOGO = false
1094MAP_LOGO_POS = BL/-54p/-54p
1095MAP_ORIGIN_X = 1i
1096MAP_ORIGIN_Y = 1i
1097MAP_POLAR_CAP = 85/90
1098MAP_SCALE_HEIGHT = 5p
1099MAP_TICK_LENGTH_PRIMARY = 5p/2.5p
1100MAP_TICK_LENGTH_SECONDARY = 15p/3.75p
1101MAP_TICK_PEN_PRIMARY = thinner,black
1102MAP_TICK_PEN_SECONDARY = thinner,black
1103MAP_TITLE_OFFSET = 14p
1104MAP_VECTOR_SHAPE = 0
1105#
1106# Projection Parameters
1107#
1108PROJ_AUX_LATITUDE = authalic
1109PROJ_ELLIPSOID = WGS-84
1110PROJ_LENGTH_UNIT = cm
1111PROJ_MEAN_RADIUS = authalic
1112PROJ_SCALE_FACTOR = default
1113#
1114# PostScript Parameters
1115#
1116PS_CHAR_ENCODING = ISOLatin1+
1117PS_COLOR_MODEL = rgb
1118PS_COMMENTS = false
1119PS_IMAGE_COMPRESS = deflate,5
1120PS_LINE_CAP = butt
1121PS_LINE_JOIN = miter
1122PS_MITER_LIMIT = 35
1123PS_MEDIA = a4
1124PS_PAGE_COLOR = white
1125PS_PAGE_ORIENTATION = portrait
1126PS_SCALE_X = 1
1127PS_SCALE_Y = 1
1128PS_TRANSPARENCY = Normal
1129#
1130# Calendar/Time Parameters
1131#
1132TIME_EPOCH = 1970-01-01T00:00:00
1133TIME_IS_INTERVAL = off
1134TIME_INTERVAL_FRACTION = 0.5
1135TIME_UNIT = s
1136TIME_WEEK_START = Monday
1137TIME_Y2K_OFFSET_YEAR = 1950
1138'''
1141def get_gmt_version(gmtdefaultsbinary, gmthomedir=None):
1142 args = [gmtdefaultsbinary]
1144 environ = os.environ.copy()
1145 environ['GMTHOME'] = gmthomedir or ''
1147 p = subprocess.Popen(
1148 args,
1149 stdout=subprocess.PIPE,
1150 stderr=subprocess.PIPE,
1151 env=environ)
1153 (stdout, stderr) = p.communicate()
1154 m = re.search(br'(\d+(\.\d+)*)', stderr) \
1155 or re.search(br'# GMT (\d+(\.\d+)*)', stdout)
1157 if not m:
1158 raise GMTInstallationProblem(
1159 "Can't extract version number from output of %s."
1160 % gmtdefaultsbinary)
1162 return str(m.group(1).decode('ascii'))
1165def detect_gmt_installations():
1167 installations = {}
1168 errmesses = []
1170 # GMT 4.x:
1171 try:
1172 p = subprocess.Popen(
1173 ['GMT'],
1174 stdout=subprocess.PIPE,
1175 stderr=subprocess.PIPE)
1177 (stdout, stderr) = p.communicate()
1179 m = re.search(br'Version\s+(\d+(\.\d+)*)', stderr, re.M)
1180 if not m:
1181 raise GMTInstallationProblem(
1182 "Can't get version number from output of GMT.")
1184 version = str(m.group(1).decode('ascii'))
1185 if version[0] != '5':
1187 m = re.search(br'^\s+executables\s+(.+)$', stderr, re.M)
1188 if not m:
1189 raise GMTInstallationProblem(
1190 "Can't extract executables dir from output of GMT.")
1192 gmtbin = str(m.group(1).decode('ascii'))
1194 m = re.search(br'^\s+shared data\s+(.+)$', stderr, re.M)
1195 if not m:
1196 raise GMTInstallationProblem(
1197 "Can't extract shared dir from output of GMT.")
1199 gmtshare = str(m.group(1).decode('ascii'))
1200 if not gmtshare.endswith('/share'):
1201 raise GMTInstallationProblem(
1202 "Can't determine GMTHOME from output of GMT.")
1204 gmthome = gmtshare[:-6]
1206 installations[version] = {
1207 'home': gmthome,
1208 'bin': gmtbin}
1210 except OSError as e:
1211 errmesses.append(('GMT', str(e)))
1213 try:
1214 version = str(subprocess.check_output(
1215 ['gmt', '--version']).strip().decode('ascii')).split('_')[0]
1216 gmtbin = str(subprocess.check_output(
1217 ['gmt', '--show-bindir']).strip().decode('ascii'))
1218 installations[version] = {
1219 'bin': gmtbin}
1221 except (OSError, subprocess.CalledProcessError) as e:
1222 errmesses.append(('gmt', str(e)))
1224 if not installations:
1225 s = []
1226 for (progname, errmess) in errmesses:
1227 s.append('Cannot start "%s" executable: %s' % (progname, errmess))
1229 raise GMTInstallationProblem(', '.join(s))
1231 return installations
1234def appropriate_defaults_version(version):
1235 avails = sorted(_gmt_defaults_by_version.keys(), key=key_version)
1236 for iavail, avail in enumerate(avails):
1237 if key_version(version) == key_version(avail):
1238 return version
1240 elif key_version(version) < key_version(avail):
1241 return avails[max(0, iavail-1)]
1243 return avails[-1]
1246def gmt_default_config(version):
1247 '''
1248 Get default GMT configuration dict for given version.
1249 '''
1251 xversion = appropriate_defaults_version(version)
1253 # if not version in _gmt_defaults_by_version:
1254 # raise GMTError('No GMT defaults for version %s found' % version)
1256 gmt_defaults = _gmt_defaults_by_version[xversion]
1258 d = {}
1259 for line in gmt_defaults.splitlines():
1260 sline = line.strip()
1261 if not sline or sline.startswith('#'):
1262 continue
1264 k, v = sline.split('=', 1)
1265 d[k.strip()] = v.strip()
1267 return d
1270def diff_defaults(v1, v2):
1271 d1 = gmt_default_config(v1)
1272 d2 = gmt_default_config(v2)
1273 for k in d1:
1274 if k not in d2:
1275 print('%s not in %s' % (k, v2))
1276 else:
1277 if d1[k] != d2[k]:
1278 print('%s %s = %s' % (v1, k, d1[k]))
1279 print('%s %s = %s' % (v2, k, d2[k]))
1281 for k in d2:
1282 if k not in d1:
1283 print('%s not in %s' % (k, v1))
1285# diff_defaults('4.5.2', '4.5.3')
1288def check_gmt_installation(installation):
1290 home_dir = installation.get('home', None)
1291 bin_dir = installation['bin']
1292 version = installation['version']
1294 for d in home_dir, bin_dir:
1295 if d is not None:
1296 if not os.path.exists(d):
1297 logging.error(('Directory does not exist: %s\n'
1298 'Check your GMT installation.') % d)
1300 major_version = version.split('.')[0]
1302 if major_version not in ['5', '6']:
1303 gmtdefaults = pjoin(bin_dir, 'gmtdefaults')
1305 versionfound = get_gmt_version(gmtdefaults, home_dir)
1307 if versionfound != version:
1308 raise GMTInstallationProblem((
1309 'Expected GMT version %s but found version %s.\n'
1310 '(Looking at output of %s)') % (
1311 version, versionfound, gmtdefaults))
1314def get_gmt_installation(version):
1315 setup_gmt_installations()
1316 if version != 'newest' and version not in _gmt_installations:
1317 logging.warn('GMT version %s not installed, taking version %s instead'
1318 % (version, newest_installed_gmt_version()))
1320 version = 'newest'
1322 if version == 'newest':
1323 version = newest_installed_gmt_version()
1325 installation = dict(_gmt_installations[version])
1327 return installation
1330def setup_gmt_installations():
1331 if not setup_gmt_installations.have_done:
1332 if not _gmt_installations:
1334 _gmt_installations.update(detect_gmt_installations())
1336 # store defaults as dicts into the gmt installations dicts
1337 for version, installation in _gmt_installations.items():
1338 installation['defaults'] = gmt_default_config(version)
1339 installation['version'] = version
1341 for installation in _gmt_installations.values():
1342 check_gmt_installation(installation)
1344 setup_gmt_installations.have_done = True
1347setup_gmt_installations.have_done = False
1349_paper_sizes_a = '''A0 2380 3368
1350 A1 1684 2380
1351 A2 1190 1684
1352 A3 842 1190
1353 A4 595 842
1354 A5 421 595
1355 A6 297 421
1356 A7 210 297
1357 A8 148 210
1358 A9 105 148
1359 A10 74 105
1360 B0 2836 4008
1361 B1 2004 2836
1362 B2 1418 2004
1363 B3 1002 1418
1364 B4 709 1002
1365 B5 501 709
1366 archA 648 864
1367 archB 864 1296
1368 archC 1296 1728
1369 archD 1728 2592
1370 archE 2592 3456
1371 flsa 612 936
1372 halfletter 396 612
1373 note 540 720
1374 letter 612 792
1375 legal 612 1008
1376 11x17 792 1224
1377 ledger 1224 792'''
1380_paper_sizes = {}
1383def setup_paper_sizes():
1384 if not _paper_sizes:
1385 for line in _paper_sizes_a.splitlines():
1386 k, w, h = line.split()
1387 _paper_sizes[k.lower()] = float(w), float(h)
1390def get_paper_size(k):
1391 setup_paper_sizes()
1392 return _paper_sizes[k.lower().rstrip('+')]
1395def all_paper_sizes():
1396 setup_paper_sizes()
1397 return _paper_sizes
1400def measure_unit(gmt_config):
1401 for k in ['MEASURE_UNIT', 'PROJ_LENGTH_UNIT']:
1402 if k in gmt_config:
1403 return gmt_config[k]
1405 raise GmtPyError('cannot get measure unit / proj length unit from config')
1408def paper_media(gmt_config):
1409 for k in ['PAPER_MEDIA', 'PS_MEDIA']:
1410 if k in gmt_config:
1411 return gmt_config[k]
1413 raise GmtPyError('cannot get paper media from config')
1416def page_orientation(gmt_config):
1417 for k in ['PAGE_ORIENTATION', 'PS_PAGE_ORIENTATION']:
1418 if k in gmt_config:
1419 return gmt_config[k]
1421 raise GmtPyError('cannot get paper orientation from config')
1424def make_bbox(width, height, gmt_config, margins=(0.8, 0.8, 0.8, 0.8)):
1426 leftmargin, topmargin, rightmargin, bottommargin = margins
1427 portrait = page_orientation(gmt_config).lower() == 'portrait'
1429 paper_size = get_paper_size(paper_media(gmt_config))
1430 if not portrait:
1431 paper_size = paper_size[1], paper_size[0]
1433 xoffset = (paper_size[0] - (width + leftmargin + rightmargin)) / \
1434 2.0 + leftmargin
1435 yoffset = (paper_size[1] - (height + topmargin + bottommargin)) / \
1436 2.0 + bottommargin
1438 if portrait:
1439 bb1 = int((xoffset - leftmargin))
1440 bb2 = int((yoffset - bottommargin))
1441 bb3 = bb1 + int((width+leftmargin+rightmargin))
1442 bb4 = bb2 + int((height+topmargin+bottommargin))
1443 else:
1444 bb1 = int((yoffset - topmargin))
1445 bb2 = int((xoffset - leftmargin))
1446 bb3 = bb1 + int((height+topmargin+bottommargin))
1447 bb4 = bb2 + int((width+leftmargin+rightmargin))
1449 return xoffset, yoffset, (bb1, bb2, bb3, bb4)
1452def gmtdefaults_as_text(version='newest'):
1454 '''
1455 Get the built-in gmtdefaults.
1456 '''
1458 if version not in _gmt_installations:
1459 logging.warn('GMT version %s not installed, taking version %s instead'
1460 % (version, newest_installed_gmt_version()))
1461 version = 'newest'
1463 if version == 'newest':
1464 version = newest_installed_gmt_version()
1466 return _gmt_defaults_by_version[version]
1469def savegrd(x, y, z, filename, title=None, naming='xy'):
1470 '''
1471 Write COARDS compliant netcdf (grd) file.
1472 '''
1474 assert y.size, x.size == z.shape
1475 ny, nx = z.shape
1476 nc = netcdf.netcdf_file(filename, 'w')
1477 assert naming in ('xy', 'lonlat')
1479 if naming == 'xy':
1480 kx, ky = 'x', 'y'
1481 else:
1482 kx, ky = 'lon', 'lat'
1484 nc.node_offset = 0
1485 if title is not None:
1486 nc.title = title
1488 nc.Conventions = 'COARDS/CF-1.0'
1489 nc.createDimension(kx, nx)
1490 nc.createDimension(ky, ny)
1492 xvar = nc.createVariable(kx, 'd', (kx,))
1493 yvar = nc.createVariable(ky, 'd', (ky,))
1494 if naming == 'xy':
1495 xvar.long_name = kx
1496 yvar.long_name = ky
1497 else:
1498 xvar.long_name = 'longitude'
1499 xvar.units = 'degrees_east'
1500 yvar.long_name = 'latitude'
1501 yvar.units = 'degrees_north'
1503 zvar = nc.createVariable('z', 'd', (ky, kx))
1505 xvar[:] = x.astype(num.float64)
1506 yvar[:] = y.astype(num.float64)
1507 zvar[:] = z.astype(num.float64)
1509 nc.close()
1512def to_array(var):
1513 arr = var[:].copy()
1514 if hasattr(var, 'scale_factor'):
1515 arr *= var.scale_factor
1517 if hasattr(var, 'add_offset'):
1518 arr += var.add_offset
1520 return arr
1523def loadgrd(filename):
1524 '''
1525 Read COARDS compliant netcdf (grd) file.
1526 '''
1528 nc = netcdf.netcdf_file(filename, 'r')
1529 vkeys = list(nc.variables.keys())
1530 kx = 'x'
1531 ky = 'y'
1532 if 'lon' in vkeys:
1533 kx = 'lon'
1534 if 'lat' in vkeys:
1535 ky = 'lat'
1537 kz = 'z'
1538 if 'altitude' in vkeys:
1539 kz = 'altitude'
1541 x = to_array(nc.variables[kx])
1542 y = to_array(nc.variables[ky])
1543 z = to_array(nc.variables[kz])
1545 nc.close()
1546 return x, y, z
1549def centers_to_edges(asorted):
1550 return (asorted[1:] + asorted[:-1])/2.
1553def nvals(asorted):
1554 eps = (asorted[-1]-asorted[0])/asorted.size
1555 return num.sum(asorted[1:] - asorted[:-1] >= eps) + 1
1558def guess_vals(asorted):
1559 eps = (asorted[-1]-asorted[0])/asorted.size
1560 indis = num.nonzero(asorted[1:] - asorted[:-1] >= eps)[0]
1561 indis = num.concatenate((num.array([0]), indis+1,
1562 num.array([asorted.size])))
1563 asum = num.zeros(asorted.size+1)
1564 asum[1:] = num.cumsum(asorted)
1565 return (asum[indis[1:]] - asum[indis[:-1]]) / (indis[1:]-indis[:-1])
1568def blockmean(asorted, b):
1569 indis = num.nonzero(asorted[1:] - asorted[:-1])[0]
1570 indis = num.concatenate((num.array([0]), indis+1,
1571 num.array([asorted.size])))
1572 bsum = num.zeros(b.size+1)
1573 bsum[1:] = num.cumsum(b)
1574 return (
1575 asorted[indis[:-1]],
1576 (bsum[indis[1:]] - bsum[indis[:-1]]) / (indis[1:]-indis[:-1]))
1579def griddata_regular(x, y, z, xvals, yvals):
1580 nx, ny = xvals.size, yvals.size
1581 xindi = num.digitize(x, centers_to_edges(xvals))
1582 yindi = num.digitize(y, centers_to_edges(yvals))
1584 zindi = yindi*nx+xindi
1585 order = num.argsort(zindi)
1586 z = z[order]
1587 zindi = zindi[order]
1589 zindi, z = blockmean(zindi, z)
1590 znew = num.empty(nx*ny, dtype=float)
1591 znew[:] = num.nan
1592 znew[zindi] = z
1593 return znew.reshape(ny, nx)
1596def guess_field_size(x_sorted, y_sorted, z=None, mode=None):
1597 critical_fraction = 1./num.e - 0.014*3
1598 xs = x_sorted
1599 ys = y_sorted
1600 nxs, nys = nvals(xs), nvals(ys)
1601 if mode == 'nonrandom':
1602 return nxs, nys, 0
1603 elif xs.size == nxs*nys:
1604 # exact match
1605 return nxs, nys, 0
1606 elif nxs >= xs.size*critical_fraction and nys >= xs.size*critical_fraction:
1607 # possibly randomly sampled
1608 nxs = int(math.sqrt(xs.size))
1609 nys = nxs
1610 return nxs, nys, 2
1611 else:
1612 return nxs, nys, 1
1615def griddata_auto(x, y, z, mode=None):
1616 '''
1617 Grid tabular XYZ data by binning.
1619 This function does some extra work to guess the size of the grid. This
1620 should work fine if the input values are already defined on an rectilinear
1621 grid, even if data points are missing or duplicated. This routine also
1622 tries to detect a random distribution of input data and in that case
1623 creates a grid of size sqrt(N) x sqrt(N).
1625 The points do not have to be given in any particular order. Grid nodes
1626 without data are assigned the NaN value. If multiple data points map to the
1627 same grid node, their average is assigned to the grid node.
1628 '''
1630 x, y, z = [num.asarray(X) for X in (x, y, z)]
1631 assert x.size == y.size == z.size
1632 xs, ys = num.sort(x), num.sort(y)
1633 nx, ny, badness = guess_field_size(xs, ys, z, mode=mode)
1634 if badness <= 1:
1635 xf = guess_vals(xs)
1636 yf = guess_vals(ys)
1637 zf = griddata_regular(x, y, z, xf, yf)
1638 else:
1639 xf = num.linspace(xs[0], xs[-1], nx)
1640 yf = num.linspace(ys[0], ys[-1], ny)
1641 zf = griddata_regular(x, y, z, xf, yf)
1643 return xf, yf, zf
1646def tabledata(xf, yf, zf):
1647 assert yf.size, xf.size == zf.shape
1648 x = num.tile(xf, yf.size)
1649 y = num.repeat(yf, xf.size)
1650 z = zf.flatten()
1651 return x, y, z
1654def double1d(a):
1655 a2 = num.empty(a.size*2-1)
1656 a2[::2] = a
1657 a2[1::2] = (a[:-1] + a[1:])/2.
1658 return a2
1661def double2d(f):
1662 f2 = num.empty((f.shape[0]*2-1, f.shape[1]*2-1))
1663 f2[:, :] = num.nan
1664 f2[::2, ::2] = f
1665 f2[1::2, ::2] = (f[:-1, :] + f[1:, :])/2.
1666 f2[::2, 1::2] = (f[:, :-1] + f[:, 1:])/2.
1667 f2[1::2, 1::2] = (f[:-1, :-1] + f[1:, :-1] + f[:-1, 1:] + f[1:, 1:])/4.
1668 diag = f2[1::2, 1::2]
1669 diagA = (f[:-1, :-1] + f[1:, 1:]) / 2.
1670 diagB = (f[1:, :-1] + f[:-1, 1:]) / 2.
1671 f2[1::2, 1::2] = num.where(num.isnan(diag), diagA, diag)
1672 f2[1::2, 1::2] = num.where(num.isnan(diag), diagB, diag)
1673 return f2
1676def doublegrid(x, y, z):
1677 x2 = double1d(x)
1678 y2 = double1d(y)
1679 z2 = double2d(z)
1680 return x2, y2, z2
1683class Guru(object):
1684 '''
1685 Abstract base class providing template interpolation, accessible as
1686 attributes.
1688 Classes deriving from this one, have to implement a :py:meth:`get_params`
1689 method, which is called to get a dict to do ordinary
1690 ``"%(key)x"``-substitutions. The deriving class must also provide a dict
1691 with the templates.
1692 '''
1694 def __init__(self):
1695 self.templates = {}
1697 def fill(self, templates, **kwargs):
1698 params = self.get_params(**kwargs)
1699 strings = [t % params for t in templates]
1700 return strings
1702 # hand through templates dict
1703 def __getitem__(self, template_name):
1704 return self.templates[template_name]
1706 def __setitem__(self, template_name, template):
1707 self.templates[template_name] = template
1709 def __contains__(self, template_name):
1710 return template_name in self.templates
1712 def __iter__(self):
1713 return iter(self.templates)
1715 def __len__(self):
1716 return len(self.templates)
1718 def __delitem__(self, template_name):
1719 del(self.templates[template_name])
1721 def _simple_fill(self, template_names, **kwargs):
1722 templates = [self.templates[n] for n in template_names]
1723 return self.fill(templates, **kwargs)
1725 def __getattr__(self, template_names):
1726 if [n for n in template_names if n not in self.templates]:
1727 raise AttributeError(template_names)
1729 def f(**kwargs):
1730 return self._simple_fill(template_names, **kwargs)
1732 return f
1735def nice_value(x):
1736 '''
1737 Round ``x`` to nice value.
1738 '''
1740 exp = 1.0
1741 sign = 1
1742 if x < 0.0:
1743 x = -x
1744 sign = -1
1745 while x >= 1.0:
1746 x /= 10.0
1747 exp *= 10.0
1748 while x < 0.1:
1749 x *= 10.0
1750 exp /= 10.0
1752 if x >= 0.75:
1753 return sign * 1.0 * exp
1754 if x >= 0.375:
1755 return sign * 0.5 * exp
1756 if x >= 0.225:
1757 return sign * 0.25 * exp
1758 if x >= 0.15:
1759 return sign * 0.2 * exp
1761 return sign * 0.1 * exp
1764class AutoScaler(object):
1765 '''
1766 Tunable 1D autoscaling based on data range.
1768 Instances of this class may be used to determine nice minima, maxima and
1769 increments for ax annotations, as well as suitable common exponents for
1770 notation.
1772 The autoscaling process is guided by the following public attributes:
1774 .. py:attribute:: approx_ticks
1776 Approximate number of increment steps (tickmarks) to generate.
1778 .. py:attribute:: mode
1780 Mode of operation: one of ``'auto'``, ``'min-max'``, ``'0-max'``,
1781 ``'min-0'``, ``'symmetric'`` or ``'off'``.
1783 ================ ==================================================
1784 mode description
1785 ================ ==================================================
1786 ``'auto'``: Look at data range and choose one of the choices
1787 below.
1788 ``'min-max'``: Output range is selected to include data range.
1789 ``'0-max'``: Output range shall start at zero and end at data
1790 max.
1791 ``'min-0'``: Output range shall start at data min and end at
1792 zero.
1793 ``'symmetric'``: Output range shall by symmetric by zero.
1794 ``'off'``: Similar to ``'min-max'``, but snap and space are
1795 disabled, such that the output range always
1796 exactly matches the data range.
1797 ================ ==================================================
1799 .. py:attribute:: exp
1801 If defined, override automatically determined exponent for notation
1802 by the given value.
1804 .. py:attribute:: snap
1806 If set to True, snap output range to multiples of increment. This
1807 parameter has no effect, if mode is set to ``'off'``.
1809 .. py:attribute:: inc
1811 If defined, override automatically determined tick increment by the
1812 given value.
1814 .. py:attribute:: space
1816 Add some padding to the range. The value given, is the fraction by
1817 which the output range is increased on each side. If mode is
1818 ``'0-max'`` or ``'min-0'``, the end at zero is kept fixed at zero.
1819 This parameter has no effect if mode is set to ``'off'``.
1821 .. py:attribute:: exp_factor
1823 Exponent of notation is chosen to be a multiple of this value.
1825 .. py:attribute:: no_exp_interval:
1827 Range of exponent, for which no exponential notation is allowed.
1829 '''
1831 def __init__(
1832 self,
1833 approx_ticks=7.0,
1834 mode='auto',
1835 exp=None,
1836 snap=False,
1837 inc=None,
1838 space=0.0,
1839 exp_factor=3,
1840 no_exp_interval=(-3, 5)):
1842 '''
1843 Create new AutoScaler instance.
1845 The parameters are described in the AutoScaler documentation.
1846 '''
1848 self.approx_ticks = approx_ticks
1849 self.mode = mode
1850 self.exp = exp
1851 self.snap = snap
1852 self.inc = inc
1853 self.space = space
1854 self.exp_factor = exp_factor
1855 self.no_exp_interval = no_exp_interval
1857 def make_scale(self, data_range, override_mode=None):
1859 '''
1860 Get nice minimum, maximum and increment for given data range.
1862 Returns ``(minimum, maximum, increment)`` or ``(maximum, minimum,
1863 -increment)``, depending on whether data_range is ``(data_min,
1864 data_max)`` or ``(data_max, data_min)``. If ``override_mode`` is
1865 defined, the mode attribute is temporarily overridden by the given
1866 value.
1867 '''
1869 data_min = min(data_range)
1870 data_max = max(data_range)
1872 is_reverse = (data_range[0] > data_range[1])
1874 a = self.mode
1875 if self.mode == 'auto':
1876 a = self.guess_autoscale_mode(data_min, data_max)
1878 if override_mode is not None:
1879 a = override_mode
1881 mi, ma = 0, 0
1882 if a == 'off':
1883 mi, ma = data_min, data_max
1884 elif a == '0-max':
1885 mi = 0.0
1886 if data_max > 0.0:
1887 ma = data_max
1888 else:
1889 ma = 1.0
1890 elif a == 'min-0':
1891 ma = 0.0
1892 if data_min < 0.0:
1893 mi = data_min
1894 else:
1895 mi = -1.0
1896 elif a == 'min-max':
1897 mi, ma = data_min, data_max
1898 elif a == 'symmetric':
1899 m = max(abs(data_min), abs(data_max))
1900 mi = -m
1901 ma = m
1903 nmi = mi
1904 if (mi != 0. or a == 'min-max') and a != 'off':
1905 nmi = mi - self.space*(ma-mi)
1907 nma = ma
1908 if (ma != 0. or a == 'min-max') and a != 'off':
1909 nma = ma + self.space*(ma-mi)
1911 mi, ma = nmi, nma
1913 if mi == ma and a != 'off':
1914 mi -= 1.0
1915 ma += 1.0
1917 # make nice tick increment
1918 if self.inc is not None:
1919 inc = self.inc
1920 else:
1921 if self.approx_ticks > 0.:
1922 inc = nice_value((ma-mi) / self.approx_ticks)
1923 else:
1924 inc = nice_value((ma-mi)*10.)
1926 if inc == 0.0:
1927 inc = 1.0
1929 # snap min and max to ticks if this is wanted
1930 if self.snap and a != 'off':
1931 ma = inc * math.ceil(ma/inc)
1932 mi = inc * math.floor(mi/inc)
1934 if is_reverse:
1935 return ma, mi, -inc
1936 else:
1937 return mi, ma, inc
1939 def make_exp(self, x):
1940 '''
1941 Get nice exponent for notation of ``x``.
1943 For ax annotations, give tick increment as ``x``.
1944 '''
1946 if self.exp is not None:
1947 return self.exp
1949 x = abs(x)
1950 if x == 0.0:
1951 return 0
1953 if 10**self.no_exp_interval[0] <= x <= 10**self.no_exp_interval[1]:
1954 return 0
1956 return math.floor(math.log10(x)/self.exp_factor)*self.exp_factor
1958 def guess_autoscale_mode(self, data_min, data_max):
1959 '''
1960 Guess mode of operation, based on data range.
1962 Used to map ``'auto'`` mode to ``'0-max'``, ``'min-0'``, ``'min-max'``
1963 or ``'symmetric'``.
1964 '''
1966 a = 'min-max'
1967 if data_min >= 0.0:
1968 if data_min < data_max/2.:
1969 a = '0-max'
1970 else:
1971 a = 'min-max'
1972 if data_max <= 0.0:
1973 if data_max > data_min/2.:
1974 a = 'min-0'
1975 else:
1976 a = 'min-max'
1977 if data_min < 0.0 and data_max > 0.0:
1978 if abs((abs(data_max)-abs(data_min)) /
1979 (abs(data_max)+abs(data_min))) < 0.5:
1980 a = 'symmetric'
1981 else:
1982 a = 'min-max'
1983 return a
1986class Ax(AutoScaler):
1987 '''
1988 Ax description with autoscaling capabilities.
1990 The ax is described by the :py:class:`AutoScaler` public attributes, plus
1991 the following additional attributes (with default values given in
1992 paranthesis):
1994 .. py:attribute:: label
1996 Ax label (without unit).
1998 .. py:attribute:: unit
2000 Physical unit of the data attached to this ax.
2002 .. py:attribute:: scaled_unit
2004 (see below)
2006 .. py:attribute:: scaled_unit_factor
2008 Scaled physical unit and factor between unit and scaled_unit so that
2010 unit = scaled_unit_factor x scaled_unit.
2012 (E.g. if unit is 'm' and data is in the range of nanometers, you may
2013 want to set the scaled_unit to 'nm' and the scaled_unit_factor to
2014 1e9.)
2016 .. py:attribute:: limits
2018 If defined, fix range of ax to limits=(min,max).
2020 .. py:attribute:: masking
2022 If true and if there is a limit on the ax, while calculating ranges,
2023 the data points are masked such that data points outside of this axes
2024 limits are not used to determine the range of another dependant ax.
2026 '''
2028 def __init__(self, label='', unit='', scaled_unit_factor=1.,
2029 scaled_unit='', limits=None, masking=True, **kwargs):
2031 AutoScaler.__init__(self, **kwargs)
2032 self.label = label
2033 self.unit = unit
2034 self.scaled_unit_factor = scaled_unit_factor
2035 self.scaled_unit = scaled_unit
2036 self.limits = limits
2037 self.masking = masking
2039 def label_str(self, exp, unit):
2040 '''
2041 Get label string including the unit and multiplier.
2042 '''
2044 slabel, sunit, sexp = '', '', ''
2045 if self.label:
2046 slabel = self.label
2048 if unit or exp != 0:
2049 if exp != 0:
2050 sexp = '\\327 10@+%i@+' % exp
2051 sunit = '[ %s %s ]' % (sexp, unit)
2052 else:
2053 sunit = '[ %s ]' % unit
2055 p = []
2056 if slabel:
2057 p.append(slabel)
2059 if sunit:
2060 p.append(sunit)
2062 return ' '.join(p)
2064 def make_params(self, data_range, ax_projection=False, override_mode=None,
2065 override_scaled_unit_factor=None):
2067 '''
2068 Get minimum, maximum, increment and label string for ax display.'
2070 Returns minimum, maximum, increment and label string including unit and
2071 multiplier for given data range.
2073 If ``ax_projection`` is True, values suitable to be displayed on the ax
2074 are returned, e.g. min, max and inc are returned in scaled units.
2075 Otherwise the values are returned in the original units, without any
2076 scaling applied.
2077 '''
2079 sf = self.scaled_unit_factor
2081 if override_scaled_unit_factor is not None:
2082 sf = override_scaled_unit_factor
2084 dr_scaled = [sf*x for x in data_range]
2086 mi, ma, inc = self.make_scale(dr_scaled, override_mode=override_mode)
2087 if self.inc is not None:
2088 inc = self.inc*sf
2090 if ax_projection:
2091 exp = self.make_exp(inc)
2092 if sf == 1. and override_scaled_unit_factor is None:
2093 unit = self.unit
2094 else:
2095 unit = self.scaled_unit
2096 label = self.label_str(exp, unit)
2097 return mi/10**exp, ma/10**exp, inc/10**exp, label
2098 else:
2099 label = self.label_str(0, self.unit)
2100 return mi/sf, ma/sf, inc/sf, label
2103class ScaleGuru(Guru):
2105 '''
2106 2D/3D autoscaling and ax annotation facility.
2108 Instances of this class provide automatic determination of plot ranges,
2109 tick increments and scaled annotations, as well as label/unit handling. It
2110 can in particular be used to automatically generate the -R and -B option
2111 arguments, which are required for most GMT commands.
2113 It extends the functionality of the :py:class:`Ax` and
2114 :py:class:`AutoScaler` classes at the level, where it can not be handled
2115 anymore by looking at a single dimension of the dataset's data, e.g.:
2117 * The ability to impose a fixed aspect ratio between two axes.
2119 * Recalculation of data range on non-limited axes, when there are
2120 limits imposed on other axes.
2122 '''
2124 def __init__(self, data_tuples=None, axes=None, aspect=None,
2125 percent_interval=None, copy_from=None):
2127 Guru.__init__(self)
2129 if copy_from:
2130 self.templates = copy.deepcopy(copy_from.templates)
2131 self.axes = copy.deepcopy(copy_from.axes)
2132 self.data_ranges = copy.deepcopy(copy_from.data_ranges)
2133 self.aspect = copy_from.aspect
2135 if percent_interval is not None:
2136 from scipy.stats import scoreatpercentile as scap
2138 self.templates = dict(
2139 R='-R%(xmin)g/%(xmax)g/%(ymin)g/%(ymax)g',
2140 B='-B%(xinc)g:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen',
2141 T='-T%(zmin)g/%(zmax)g/%(zinc)g')
2143 maxdim = 2
2144 if data_tuples:
2145 maxdim = max(maxdim, max([len(dt) for dt in data_tuples]))
2146 else:
2147 if axes:
2148 maxdim = len(axes)
2149 data_tuples = [([],) * maxdim]
2150 if axes is not None:
2151 self.axes = axes
2152 else:
2153 self.axes = [Ax() for i in range(maxdim)]
2155 # sophisticated data-range calculation
2156 data_ranges = [None] * maxdim
2157 for dt_ in data_tuples:
2158 dt = num.asarray(dt_)
2159 in_range = True
2160 for ax, x in zip(self.axes, dt):
2161 if ax.limits and ax.masking:
2162 ax_limits = list(ax.limits)
2163 if ax_limits[0] is None:
2164 ax_limits[0] = -num.inf
2165 if ax_limits[1] is None:
2166 ax_limits[1] = num.inf
2167 in_range = num.logical_and(
2168 in_range,
2169 num.logical_and(ax_limits[0] <= x, x <= ax_limits[1]))
2171 for i, ax, x in zip(range(maxdim), self.axes, dt):
2173 if not ax.limits or None in ax.limits:
2174 if len(x) >= 1:
2175 if in_range is not True:
2176 xmasked = num.where(in_range, x, num.NaN)
2177 if percent_interval is None:
2178 range_this = (
2179 num.nanmin(xmasked),
2180 num.nanmax(xmasked))
2181 else:
2182 xmasked_finite = num.compress(
2183 num.isfinite(xmasked), xmasked)
2184 range_this = (
2185 scap(xmasked_finite,
2186 (100.-percent_interval)/2.),
2187 scap(xmasked_finite,
2188 100.-(100.-percent_interval)/2.))
2189 else:
2190 if percent_interval is None:
2191 range_this = num.nanmin(x), num.nanmax(x)
2192 else:
2193 xmasked_finite = num.compress(
2194 num.isfinite(xmasked), xmasked)
2195 range_this = (
2196 scap(xmasked_finite,
2197 (100.-percent_interval)/2.),
2198 scap(xmasked_finite,
2199 100.-(100.-percent_interval)/2.))
2200 else:
2201 range_this = (0., 1.)
2203 if ax.limits:
2204 if ax.limits[0] is not None:
2205 range_this = ax.limits[0], max(ax.limits[0],
2206 range_this[1])
2208 if ax.limits[1] is not None:
2209 range_this = min(ax.limits[1],
2210 range_this[0]), ax.limits[1]
2212 else:
2213 range_this = ax.limits
2215 if data_ranges[i] is None and range_this[0] <= range_this[1]:
2216 data_ranges[i] = range_this
2217 else:
2218 mi, ma = range_this
2219 if data_ranges[i] is not None:
2220 mi = min(data_ranges[i][0], mi)
2221 ma = max(data_ranges[i][1], ma)
2223 data_ranges[i] = (mi, ma)
2225 for i in range(len(data_ranges)):
2226 if data_ranges[i] is None or not (
2227 num.isfinite(data_ranges[i][0])
2228 and num.isfinite(data_ranges[i][1])):
2230 data_ranges[i] = (0., 1.)
2232 self.data_ranges = data_ranges
2233 self.aspect = aspect
2235 def copy(self):
2236 return ScaleGuru(copy_from=self)
2238 def get_params(self, ax_projection=False):
2240 '''
2241 Get dict with output parameters.
2243 For each data dimension, ax minimum, maximum, increment and a label
2244 string (including unit and exponential factor) are determined. E.g. in
2245 for the first dimension the output dict will contain the keys
2246 ``'xmin'``, ``'xmax'``, ``'xinc'``, and ``'xlabel'``.
2248 Normally, values corresponding to the scaling of the raw data are
2249 produced, but if ``ax_projection`` is ``True``, values which are
2250 suitable to be printed on the axes are returned. This means that in the
2251 latter case, the :py:attr:`Ax.scaled_unit` and
2252 :py:attr:`Ax.scaled_unit_factor` attributes as set on the axes are
2253 respected and that a common 10^x factor is factored out and put to the
2254 label string.
2255 '''
2257 xmi, xma, xinc, xlabel = self.axes[0].make_params(
2258 self.data_ranges[0], ax_projection)
2259 ymi, yma, yinc, ylabel = self.axes[1].make_params(
2260 self.data_ranges[1], ax_projection)
2261 if len(self.axes) > 2:
2262 zmi, zma, zinc, zlabel = self.axes[2].make_params(
2263 self.data_ranges[2], ax_projection)
2265 # enforce certain aspect, if needed
2266 if self.aspect is not None:
2267 xwid = xma-xmi
2268 ywid = yma-ymi
2269 if ywid < xwid*self.aspect:
2270 ymi -= (xwid*self.aspect - ywid)*0.5
2271 yma += (xwid*self.aspect - ywid)*0.5
2272 ymi, yma, yinc, ylabel = self.axes[1].make_params(
2273 (ymi, yma), ax_projection, override_mode='off',
2274 override_scaled_unit_factor=1.)
2276 elif xwid < ywid/self.aspect:
2277 xmi -= (ywid/self.aspect - xwid)*0.5
2278 xma += (ywid/self.aspect - xwid)*0.5
2279 xmi, xma, xinc, xlabel = self.axes[0].make_params(
2280 (xmi, xma), ax_projection, override_mode='off',
2281 override_scaled_unit_factor=1.)
2283 params = dict(xmin=xmi, xmax=xma, xinc=xinc, xlabel=xlabel,
2284 ymin=ymi, ymax=yma, yinc=yinc, ylabel=ylabel)
2285 if len(self.axes) > 2:
2286 params.update(dict(zmin=zmi, zmax=zma, zinc=zinc, zlabel=zlabel))
2288 return params
2291class GumSpring(object):
2293 '''
2294 Sizing policy implementing a minimal size, plus a desire to grow.
2295 '''
2297 def __init__(self, minimal=None, grow=None):
2298 self.minimal = minimal
2299 if grow is None:
2300 if minimal is None:
2301 self.grow = 1.0
2302 else:
2303 self.grow = 0.0
2304 else:
2305 self.grow = grow
2306 self.value = 1.0
2308 def get_minimal(self):
2309 if self.minimal is not None:
2310 return self.minimal
2311 else:
2312 return 0.0
2314 def get_grow(self):
2315 return self.grow
2317 def set_value(self, value):
2318 self.value = value
2320 def get_value(self):
2321 return self.value
2324def distribute(sizes, grows, space):
2325 sizes = list(sizes)
2326 gsum = sum(grows)
2327 if gsum > 0.0:
2328 for i in range(len(sizes)):
2329 sizes[i] += space*grows[i]/gsum
2330 return sizes
2333class Widget(Guru):
2335 '''
2336 Base class of the gmtpy layout system.
2338 The Widget class provides the basic functionality for the nesting and
2339 placing of elements on the output page, and maintains the sizing policies
2340 of each element. Each of the layouts defined in gmtpy is itself a Widget.
2342 Sizing of the widget is controlled by :py:meth:`get_min_size` and
2343 :py:meth:`get_grow` which should be overloaded in derived classes. The
2344 basic behaviour of a Widget instance is to have a vertical and a horizontal
2345 minimum size which default to zero, as well as a vertical and a horizontal
2346 desire to grow, represented by floats, which default to 1.0. Additionally
2347 an aspect ratio constraint may be imposed on the Widget.
2349 After layouting, the widget provides its width, height, x-offset and
2350 y-offset in various ways. Via the Guru interface (see :py:class:`Guru`
2351 class), templates for the -X, -Y and -J option arguments used by GMT
2352 arguments are provided. The defaults are suitable for plotting of linear
2353 (-JX) plots. Other projections can be selected by giving an appropriate 'J'
2354 template, or by manual construction of the -J option, e.g. by utilizing the
2355 :py:meth:`width` and :py:meth:`height` methods. The :py:meth:`bbox` method
2356 can be used to create a PostScript bounding box from the widgets border,
2357 e.g. for use in the :py:meth:`save` method of :py:class:`GMT` instances.
2359 The convention is, that all sizes are given in PostScript points.
2360 Conversion factors are provided as constants :py:const:`inch` and
2361 :py:const:`cm` in the gmtpy module.
2362 '''
2364 def __init__(self, horizontal=None, vertical=None, parent=None):
2366 '''
2367 Create new widget.
2368 '''
2370 Guru.__init__(self)
2372 self.templates = dict(
2373 X='-Xa%(xoffset)gp',
2374 Y='-Ya%(yoffset)gp',
2375 J='-JX%(width)gp/%(height)gp')
2377 if horizontal is None:
2378 self.horizontal = GumSpring()
2379 else:
2380 self.horizontal = horizontal
2382 if vertical is None:
2383 self.vertical = GumSpring()
2384 else:
2385 self.vertical = vertical
2387 self.aspect = None
2388 self.parent = parent
2389 self.dirty = True
2391 def set_parent(self, parent):
2393 '''
2394 Set the parent widget.
2396 This method should not be called directly. The :py:meth:`set_widget`
2397 methods are responsible for calling this.
2398 '''
2400 self.parent = parent
2401 self.dirtyfy()
2403 def get_parent(self):
2405 '''
2406 Get the widgets parent widget.
2407 '''
2409 return self.parent
2411 def get_root(self):
2413 '''
2414 Get the root widget in the layout hierarchy.
2415 '''
2417 if self.parent is not None:
2418 return self.get_parent()
2419 else:
2420 return self
2422 def set_horizontal(self, minimal=None, grow=None):
2424 '''
2425 Set the horizontal sizing policy of the Widget.
2428 :param minimal: new minimal width of the widget
2429 :param grow: new horizontal grow disire of the widget
2430 '''
2432 self.horizontal = GumSpring(minimal, grow)
2433 self.dirtyfy()
2435 def get_horizontal(self):
2436 return self.horizontal.get_minimal(), self.horizontal.get_grow()
2438 def set_vertical(self, minimal=None, grow=None):
2440 '''
2441 Set the horizontal sizing policy of the Widget.
2443 :param minimal: new minimal height of the widget
2444 :param grow: new vertical grow disire of the widget
2445 '''
2447 self.vertical = GumSpring(minimal, grow)
2448 self.dirtyfy()
2450 def get_vertical(self):
2451 return self.vertical.get_minimal(), self.vertical.get_grow()
2453 def set_aspect(self, aspect=None):
2455 '''
2456 Set aspect constraint on the widget.
2458 The aspect is given as height divided by width.
2459 '''
2461 self.aspect = aspect
2462 self.dirtyfy()
2464 def set_policy(self, minimal=(None, None), grow=(None, None), aspect=None):
2466 '''
2467 Shortcut to set sizing and aspect constraints in a single method
2468 call.
2469 '''
2471 self.set_horizontal(minimal[0], grow[0])
2472 self.set_vertical(minimal[1], grow[1])
2473 self.set_aspect(aspect)
2475 def get_policy(self):
2476 mh, gh = self.get_horizontal()
2477 mv, gv = self.get_vertical()
2478 return (mh, mv), (gh, gv), self.aspect
2480 def legalize(self, size, offset):
2482 '''
2483 Get legal size for widget.
2485 Returns: (new_size, new_offset)
2487 Given a box as ``size`` and ``offset``, return ``new_size`` and
2488 ``new_offset``, such that the widget's sizing and aspect constraints
2489 are fullfilled. The returned box is centered on the given input box.
2490 '''
2492 sh, sv = size
2493 oh, ov = offset
2494 shs, svs = Widget.get_min_size(self)
2495 ghs, gvs = Widget.get_grow(self)
2497 if ghs == 0.0:
2498 oh += (sh-shs)/2.
2499 sh = shs
2501 if gvs == 0.0:
2502 ov += (sv-svs)/2.
2503 sv = svs
2505 if self.aspect is not None:
2506 if sh > sv/self.aspect:
2507 oh += (sh-sv/self.aspect)/2.
2508 sh = sv/self.aspect
2509 if sv > sh*self.aspect:
2510 ov += (sv-sh*self.aspect)/2.
2511 sv = sh*self.aspect
2513 return (sh, sv), (oh, ov)
2515 def get_min_size(self):
2517 '''
2518 Get minimum size of widget.
2520 Used by the layout managers. Should be overloaded in derived classes.
2521 '''
2523 mh, mv = self.horizontal.get_minimal(), self.vertical.get_minimal()
2524 if self.aspect is not None:
2525 if mv == 0.0:
2526 return mh, mh*self.aspect
2527 elif mh == 0.0:
2528 return mv/self.aspect, mv
2529 return mh, mv
2531 def get_grow(self):
2533 '''
2534 Get widget's desire to grow.
2536 Used by the layout managers. Should be overloaded in derived classes.
2537 '''
2539 return self.horizontal.get_grow(), self.vertical.get_grow()
2541 def set_size(self, size, offset):
2543 '''
2544 Set the widget's current size.
2546 Should not be called directly. It is the layout manager's
2547 responsibility to call this.
2548 '''
2550 (sh, sv), inner_offset = self.legalize(size, offset)
2551 self.offset = inner_offset
2552 self.horizontal.set_value(sh)
2553 self.vertical.set_value(sv)
2554 self.dirty = False
2556 def __str__(self):
2558 def indent(ind, str):
2559 return ('\n'+ind).join(str.splitlines())
2560 size, offset = self.get_size()
2561 s = "%s (%g x %g) (%g, %g)\n" % ((self.__class__,) + size + offset)
2562 children = self.get_children()
2563 if children:
2564 s += '\n'.join([' ' + indent(' ', str(c)) for c in children])
2565 return s
2567 def policies_debug_str(self):
2569 def indent(ind, str):
2570 return ('\n'+ind).join(str.splitlines())
2571 mins, grows, aspect = self.get_policy()
2572 s = "%s: minimum=(%s, %s), grow=(%s, %s), aspect=%s\n" % (
2573 (self.__class__,) + mins+grows+(aspect,))
2575 children = self.get_children()
2576 if children:
2577 s += '\n'.join([' ' + indent(
2578 ' ', c.policies_debug_str()) for c in children])
2579 return s
2581 def get_corners(self, descend=False):
2583 '''
2584 Get coordinates of the corners of the widget.
2586 Returns list with coordinate tuples.
2588 If ``descend`` is True, the returned list will contain corner
2589 coordinates of all sub-widgets.
2590 '''
2592 self.do_layout()
2593 (sh, sv), (oh, ov) = self.get_size()
2594 corners = [(oh, ov), (oh+sh, ov), (oh+sh, ov+sv), (oh, ov+sv)]
2595 if descend:
2596 for child in self.get_children():
2597 corners.extend(child.get_corners(descend=True))
2598 return corners
2600 def get_sizes(self):
2602 '''
2603 Get sizes of this widget and all it's children.
2605 Returns a list with size tuples.
2606 '''
2607 self.do_layout()
2608 sizes = [self.get_size()]
2609 for child in self.get_children():
2610 sizes.extend(child.get_sizes())
2611 return sizes
2613 def do_layout(self):
2615 '''
2616 Triggers layouting of the widget hierarchy, if needed.
2617 '''
2619 if self.parent is not None:
2620 return self.parent.do_layout()
2622 if not self.dirty:
2623 return
2625 sh, sv = self.get_min_size()
2626 gh, gv = self.get_grow()
2627 if sh == 0.0 and gh != 0.0:
2628 sh = 15.*cm
2629 if sv == 0.0 and gv != 0.0:
2630 sv = 15.*cm*gv/gh * 1./golden_ratio
2631 self.set_size((sh, sv), (0., 0.))
2633 def get_children(self):
2635 '''
2636 Get sub-widgets contained in this widget.
2638 Returns a list of widgets.
2639 '''
2641 return []
2643 def get_size(self):
2645 '''
2646 Get current size and position of the widget.
2648 Triggers layouting and returns
2649 ``((width, height), (xoffset, yoffset))``
2650 '''
2652 self.do_layout()
2653 return (self.horizontal.get_value(),
2654 self.vertical.get_value()), self.offset
2656 def get_params(self):
2658 '''
2659 Get current size and position of the widget.
2661 Triggers layouting and returns dict with keys ``'xoffset'``,
2662 ``'yoffset'``, ``'width'`` and ``'height'``.
2663 '''
2665 self.do_layout()
2666 (w, h), (xo, yo) = self.get_size()
2667 return dict(xoffset=xo, yoffset=yo, width=w, height=h,
2668 width_m=w/_units['m'])
2670 def width(self):
2672 '''
2673 Get current width of the widget.
2675 Triggers layouting and returns width.
2676 '''
2678 self.do_layout()
2679 return self.horizontal.get_value()
2681 def height(self):
2683 '''
2684 Get current height of the widget.
2686 Triggers layouting and return height.
2687 '''
2689 self.do_layout()
2690 return self.vertical.get_value()
2692 def bbox(self):
2694 '''
2695 Get PostScript bounding box for this widget.
2697 Triggers layouting and returns values suitable to create PS bounding
2698 box, representing the widgets current size and position.
2699 '''
2701 self.do_layout()
2702 return (self.offset[0], self.offset[1], self.offset[0]+self.width(),
2703 self.offset[1]+self.height())
2705 def dirtyfy(self):
2707 '''
2708 Set dirty flag on top level widget in the hierarchy.
2710 Called by various methods, to indicate, that the widget hierarchy needs
2711 new layouting.
2712 '''
2714 if self.parent is not None:
2715 self.parent.dirtyfy()
2717 self.dirty = True
2720class CenterLayout(Widget):
2722 '''
2723 A layout manager which centers its single child widget.
2725 The child widget may be oversized.
2726 '''
2728 def __init__(self, horizontal=None, vertical=None):
2729 Widget.__init__(self, horizontal, vertical)
2730 self.content = Widget(horizontal=GumSpring(grow=1.),
2731 vertical=GumSpring(grow=1.), parent=self)
2733 def get_min_size(self):
2734 shs, svs = Widget.get_min_size(self)
2735 sh, sv = self.content.get_min_size()
2736 return max(shs, sh), max(svs, sv)
2738 def get_grow(self):
2739 ghs, gvs = Widget.get_grow(self)
2740 gh, gv = self.content.get_grow()
2741 return gh*ghs, gv*gvs
2743 def set_size(self, size, offset):
2744 (sh, sv), (oh, ov) = self.legalize(size, offset)
2746 shc, svc = self.content.get_min_size()
2747 ghc, gvc = self.content.get_grow()
2748 if ghc != 0.:
2749 shc = sh
2750 if gvc != 0.:
2751 svc = sv
2752 ohc = oh+(sh-shc)/2.
2753 ovc = ov+(sv-svc)/2.
2755 self.content.set_size((shc, svc), (ohc, ovc))
2756 Widget.set_size(self, (sh, sv), (oh, ov))
2758 def set_widget(self, widget=None):
2760 '''
2761 Set the child widget, which shall be centered.
2762 '''
2764 if widget is None:
2765 widget = Widget()
2767 self.content = widget
2769 widget.set_parent(self)
2771 def get_widget(self):
2772 return self.content
2774 def get_children(self):
2775 return [self.content]
2778class FrameLayout(Widget):
2780 '''
2781 A layout manager containing a center widget sorrounded by four margin
2782 widgets.
2784 ::
2786 +---------------------------+
2787 | top |
2788 +---------------------------+
2789 | | | |
2790 | left | center | right |
2791 | | | |
2792 +---------------------------+
2793 | bottom |
2794 +---------------------------+
2796 This layout manager does a little bit of extra effort to maintain the
2797 aspect constraint of the center widget, if this is set. It does so, by
2798 allowing for a bit more flexibility in the sizing of the margins. Two
2799 shortcut methods are provided to set the margin sizes in one shot:
2800 :py:meth:`set_fixed_margins` and :py:meth:`set_min_margins`. The first sets
2801 the margins to fixed sizes, while the second gives them a minimal size and
2802 a (neglectably) small desire to grow. Using the latter may be useful when
2803 setting an aspect constraint on the center widget, because this way the
2804 maximum size of the center widget may be controlled without creating empty
2805 spaces between the widgets.
2806 '''
2808 def __init__(self, horizontal=None, vertical=None):
2809 Widget.__init__(self, horizontal, vertical)
2810 mw = 3.*cm
2811 self.left = Widget(
2812 horizontal=GumSpring(grow=0.15, minimal=mw), parent=self)
2813 self.right = Widget(
2814 horizontal=GumSpring(grow=0.15, minimal=mw), parent=self)
2815 self.top = Widget(
2816 vertical=GumSpring(grow=0.15, minimal=mw/golden_ratio),
2817 parent=self)
2818 self.bottom = Widget(
2819 vertical=GumSpring(grow=0.15, minimal=mw/golden_ratio),
2820 parent=self)
2821 self.center = Widget(
2822 horizontal=GumSpring(grow=0.7), vertical=GumSpring(grow=0.7),
2823 parent=self)
2825 def set_fixed_margins(self, left, right, top, bottom):
2826 '''
2827 Give margins fixed size constraints.
2828 '''
2830 self.left.set_horizontal(left, 0)
2831 self.right.set_horizontal(right, 0)
2832 self.top.set_vertical(top, 0)
2833 self.bottom.set_vertical(bottom, 0)
2835 def set_min_margins(self, left, right, top, bottom, grow=0.0001):
2836 '''
2837 Give margins a minimal size and the possibility to grow.
2839 The desire to grow is set to a very small number.
2840 '''
2841 self.left.set_horizontal(left, grow)
2842 self.right.set_horizontal(right, grow)
2843 self.top.set_vertical(top, grow)
2844 self.bottom.set_vertical(bottom, grow)
2846 def get_min_size(self):
2847 shs, svs = Widget.get_min_size(self)
2849 sl, sr, st, sb, sc = [x.get_min_size() for x in (
2850 self.left, self.right, self.top, self.bottom, self.center)]
2851 gl, gr, gt, gb, gc = [x.get_grow() for x in (
2852 self.left, self.right, self.top, self.bottom, self.center)]
2854 shsum = sl[0]+sr[0]+sc[0]
2855 svsum = st[1]+sb[1]+sc[1]
2857 # prevent widgets from collapsing
2858 for s, g in ((sl, gl), (sr, gr), (sc, gc)):
2859 if s[0] == 0.0 and g[0] != 0.0:
2860 shsum += 0.1*cm
2862 for s, g in ((st, gt), (sb, gb), (sc, gc)):
2863 if s[1] == 0.0 and g[1] != 0.0:
2864 svsum += 0.1*cm
2866 sh = max(shs, shsum)
2867 sv = max(svs, svsum)
2869 return sh, sv
2871 def get_grow(self):
2872 ghs, gvs = Widget.get_grow(self)
2873 gh = (self.left.get_grow()[0] +
2874 self.right.get_grow()[0] +
2875 self.center.get_grow()[0]) * ghs
2876 gv = (self.top.get_grow()[1] +
2877 self.bottom.get_grow()[1] +
2878 self.center.get_grow()[1]) * gvs
2879 return gh, gv
2881 def set_size(self, size, offset):
2882 (sh, sv), (oh, ov) = self.legalize(size, offset)
2884 sl, sr, st, sb, sc = [x.get_min_size() for x in (
2885 self.left, self.right, self.top, self.bottom, self.center)]
2886 gl, gr, gt, gb, gc = [x.get_grow() for x in (
2887 self.left, self.right, self.top, self.bottom, self.center)]
2889 ah = sh - (sl[0]+sr[0]+sc[0])
2890 av = sv - (st[1]+sb[1]+sc[1])
2892 if ah < 0.0:
2893 raise GmtPyError("Container not wide enough for contents "
2894 "(FrameLayout, available: %g cm, needed: %g cm)"
2895 % (sh/cm, (sl[0]+sr[0]+sc[0])/cm))
2896 if av < 0.0:
2897 raise GmtPyError("Container not high enough for contents "
2898 "(FrameLayout, available: %g cm, needed: %g cm)"
2899 % (sv/cm, (st[1]+sb[1]+sc[1])/cm))
2901 slh, srh, sch = distribute((sl[0], sr[0], sc[0]),
2902 (gl[0], gr[0], gc[0]), ah)
2903 stv, sbv, scv = distribute((st[1], sb[1], sc[1]),
2904 (gt[1], gb[1], gc[1]), av)
2906 if self.center.aspect is not None:
2907 ahm = sh - (sl[0]+sr[0] + scv/self.center.aspect)
2908 avm = sv - (st[1]+sb[1] + sch*self.center.aspect)
2909 if 0.0 < ahm < ah:
2910 slh, srh, sch = distribute(
2911 (sl[0], sr[0], scv/self.center.aspect),
2912 (gl[0], gr[0], 0.0), ahm)
2914 elif 0.0 < avm < av:
2915 stv, sbv, scv = distribute((st[1], sb[1],
2916 sch*self.center.aspect),
2917 (gt[1], gb[1], 0.0), avm)
2919 ah = sh - (slh+srh+sch)
2920 av = sv - (stv+sbv+scv)
2922 oh += ah/2.
2923 ov += av/2.
2924 sh -= ah
2925 sv -= av
2927 self.left.set_size((slh, scv), (oh, ov+sbv))
2928 self.right.set_size((srh, scv), (oh+slh+sch, ov+sbv))
2929 self.top.set_size((sh, stv), (oh, ov+sbv+scv))
2930 self.bottom.set_size((sh, sbv), (oh, ov))
2931 self.center.set_size((sch, scv), (oh+slh, ov+sbv))
2932 Widget.set_size(self, (sh, sv), (oh, ov))
2934 def set_widget(self, which='center', widget=None):
2936 '''
2937 Set one of the sub-widgets.
2939 ``which`` should be one of ``'left'``, ``'right'``, ``'top'``,
2940 ``'bottom'`` or ``'center'``.
2941 '''
2943 if widget is None:
2944 widget = Widget()
2946 if which in ('left', 'right', 'top', 'bottom', 'center'):
2947 self.__dict__[which] = widget
2948 else:
2949 raise GmtPyError('No such sub-widget: %s' % which)
2951 widget.set_parent(self)
2953 def get_widget(self, which='center'):
2955 '''
2956 Get one of the sub-widgets.
2958 ``which`` should be one of ``'left'``, ``'right'``, ``'top'``,
2959 ``'bottom'`` or ``'center'``.
2960 '''
2962 if which in ('left', 'right', 'top', 'bottom', 'center'):
2963 return self.__dict__[which]
2964 else:
2965 raise GmtPyError('No such sub-widget: %s' % which)
2967 def get_children(self):
2968 return [self.left, self.right, self.top, self.bottom, self.center]
2971class GridLayout(Widget):
2973 '''
2974 A layout manager which arranges its sub-widgets in a grid.
2976 The grid spacing is flexible and based on the sizing policies of the
2977 contained sub-widgets. If an equidistant grid is needed, the sizing
2978 policies of the sub-widgets have to be set equally.
2980 The height of each row and the width of each column is derived from the
2981 sizing policy of the largest sub-widget in the row or column in question.
2982 The algorithm is not very sophisticated, so conflicting sizing policies
2983 might not be resolved optimally.
2984 '''
2986 def __init__(self, nx=2, ny=2, horizontal=None, vertical=None):
2988 '''
2989 Create new grid layout with ``nx`` columns and ``ny`` rows.
2990 '''
2992 Widget.__init__(self, horizontal, vertical)
2993 self.grid = []
2994 for iy in range(ny):
2995 row = []
2996 for ix in range(nx):
2997 w = Widget(parent=self)
2998 row.append(w)
3000 self.grid.append(row)
3002 def sub_min_sizes_as_array(self):
3003 esh = num.array(
3004 [[w.get_min_size()[0] for w in row] for row in self.grid],
3005 dtype=float)
3006 esv = num.array(
3007 [[w.get_min_size()[1] for w in row] for row in self.grid],
3008 dtype=float)
3009 return esh, esv
3011 def sub_grows_as_array(self):
3012 egh = num.array(
3013 [[w.get_grow()[0] for w in row] for row in self.grid],
3014 dtype=float)
3015 egv = num.array(
3016 [[w.get_grow()[1] for w in row] for row in self.grid],
3017 dtype=float)
3018 return egh, egv
3020 def get_min_size(self):
3021 sh, sv = Widget.get_min_size(self)
3022 esh, esv = self.sub_min_sizes_as_array()
3023 if esh.size != 0:
3024 sh = max(sh, num.sum(esh.max(0)))
3025 if esv.size != 0:
3026 sv = max(sv, num.sum(esv.max(1)))
3027 return sh, sv
3029 def get_grow(self):
3030 ghs, gvs = Widget.get_grow(self)
3031 egh, egv = self.sub_grows_as_array()
3032 if egh.size != 0:
3033 gh = num.sum(egh.max(0))*ghs
3034 else:
3035 gh = 1.0
3036 if egv.size != 0:
3037 gv = num.sum(egv.max(1))*gvs
3038 else:
3039 gv = 1.0
3040 return gh, gv
3042 def set_size(self, size, offset):
3043 (sh, sv), (oh, ov) = self.legalize(size, offset)
3044 esh, esv = self.sub_min_sizes_as_array()
3045 egh, egv = self.sub_grows_as_array()
3047 # available additional space
3048 empty = esh.size == 0
3050 if not empty:
3051 ah = sh - num.sum(esh.max(0))
3052 av = sv - num.sum(esv.max(1))
3053 else:
3054 av = sv
3055 ah = sh
3057 if ah < 0.0:
3058 raise GmtPyError("Container not wide enough for contents "
3059 "(GridLayout, available: %g cm, needed: %g cm)"
3060 % (sh/cm, (num.sum(esh.max(0)))/cm))
3061 if av < 0.0:
3062 raise GmtPyError("Container not high enough for contents "
3063 "(GridLayout, available: %g cm, needed: %g cm)"
3064 % (sv/cm, (num.sum(esv.max(1)))/cm))
3066 nx, ny = esh.shape
3068 if not empty:
3069 # distribute additional space on rows and columns
3070 # according to grow weights and minimal sizes
3071 gsh = egh.sum(1)[:, num.newaxis].repeat(ny, axis=1)
3072 nesh = esh.copy()
3073 nesh += num.where(gsh > 0.0, ah*egh/gsh, 0.0)
3075 nsh = num.maximum(nesh.max(0), esh.max(0))
3077 gsv = egv.sum(0)[num.newaxis, :].repeat(nx, axis=0)
3078 nesv = esv.copy()
3079 nesv += num.where(gsv > 0.0, av*egv/gsv, 0.0)
3080 nsv = num.maximum(nesv.max(1), esv.max(1))
3082 ah = sh - sum(nsh)
3083 av = sv - sum(nsv)
3085 oh += ah/2.
3086 ov += av/2.
3087 sh -= ah
3088 sv -= av
3090 # resize child widgets
3091 neov = ov + sum(nsv)
3092 for row, nesv in zip(self.grid, nsv):
3093 neov -= nesv
3094 neoh = oh
3095 for w, nesh in zip(row, nsh):
3096 w.set_size((nesh, nesv), (neoh, neov))
3097 neoh += nesh
3099 Widget.set_size(self, (sh, sv), (oh, ov))
3101 def set_widget(self, ix, iy, widget=None):
3103 '''
3104 Set one of the sub-widgets.
3106 Sets the sub-widget in column ``ix`` and row ``iy``. The indices are
3107 counted from zero.
3108 '''
3110 if widget is None:
3111 widget = Widget()
3113 self.grid[iy][ix] = widget
3114 widget.set_parent(self)
3116 def get_widget(self, ix, iy):
3118 '''
3119 Get one of the sub-widgets.
3121 Gets the sub-widget from column ``ix`` and row ``iy``. The indices are
3122 counted from zero.
3123 '''
3125 return self.grid[iy][ix]
3127 def get_children(self):
3128 children = []
3129 for row in self.grid:
3130 children.extend(row)
3132 return children
3135def is_gmt5(version='newest'):
3136 return get_gmt_installation(version)['version'][0] in ['5', '6']
3139def aspect_for_projection(gmtversion, *args, **kwargs):
3141 gmt = GMT(version=gmtversion, eps_mode=True)
3143 if gmt.is_gmt5():
3144 gmt.psbasemap('-B+gblack', finish=True, *args, **kwargs)
3145 fn = gmt.tempfilename('test.eps')
3146 gmt.save(fn, crop_eps_mode=True)
3147 with open(fn, 'rb') as f:
3148 s = f.read()
3150 l, b, r, t = get_bbox(s)
3151 else:
3152 gmt.psbasemap('-G0', finish=True, *args, **kwargs)
3153 l, b, r, t = gmt.bbox()
3155 return (t-b)/(r-l)
3158def text_box(
3159 text, font=0, font_size=12., angle=0, gmtversion='newest', **kwargs):
3161 gmt = GMT(version=gmtversion)
3162 if gmt.is_gmt5():
3163 row = [0, 0, text]
3164 farg = ['-F+f%gp,%s,%s+j%s' % (font_size, font, 'black', 'BL')]
3165 else:
3166 row = [0, 0, font_size, 0, font, 'BL', text]
3167 farg = []
3169 gmt.pstext(
3170 in_rows=[row],
3171 finish=True,
3172 R=(0, 1, 0, 1),
3173 J='x10p',
3174 N=True,
3175 *farg,
3176 **kwargs)
3178 fn = gmt.tempfilename() + '.ps'
3179 gmt.save(fn)
3181 (_, stderr) = subprocess.Popen(
3182 ['gs', '-q', '-dNOPAUSE', '-dBATCH', '-r720', '-sDEVICE=bbox', fn],
3183 stderr=subprocess.PIPE).communicate()
3185 dx, dy = None, None
3186 for line in stderr.splitlines():
3187 if line.startswith(b'%%HiResBoundingBox:'):
3188 l, b, r, t = [float(x) for x in line.split()[-4:]]
3189 dx, dy = r-l, t-b
3190 break
3192 return dx, dy
3195class TableLiner(object):
3196 '''
3197 Utility class to turn tables into lines.
3198 '''
3200 def __init__(self, in_columns=None, in_rows=None, encoding='utf-8'):
3201 self.in_columns = in_columns
3202 self.in_rows = in_rows
3203 self.encoding = encoding
3205 def __iter__(self):
3206 if self.in_columns is not None:
3207 for row in zip(*self.in_columns):
3208 yield (' '.join([newstr(x) for x in row])+'\n').encode(
3209 self.encoding)
3211 if self.in_rows is not None:
3212 for row in self.in_rows:
3213 yield (' '.join([newstr(x) for x in row])+'\n').encode(
3214 self.encoding)
3217class LineStreamChopper(object):
3218 '''
3219 File-like object to buffer data.
3220 '''
3222 def __init__(self, liner):
3223 self.chopsize = None
3224 self.liner = liner
3225 self.chop_iterator = None
3226 self.closed = False
3228 def _chopiter(self):
3229 buf = BytesIO()
3230 for line in self.liner:
3231 buf.write(line)
3232 buflen = buf.tell()
3233 if self.chopsize is not None and buflen >= self.chopsize:
3234 buf.seek(0)
3235 while buf.tell() <= buflen-self.chopsize:
3236 yield buf.read(self.chopsize)
3238 newbuf = BytesIO()
3239 newbuf.write(buf.read())
3240 buf.close()
3241 buf = newbuf
3243 yield(buf.getvalue())
3244 buf.close()
3246 def read(self, size=None):
3247 if self.closed:
3248 raise ValueError('Cannot read from closed LineStreamChopper.')
3249 if self.chop_iterator is None:
3250 self.chopsize = size
3251 self.chop_iterator = self._chopiter()
3253 self.chopsize = size
3254 try:
3255 return next(self.chop_iterator)
3256 except StopIteration:
3257 return ''
3259 def close(self):
3260 self.chopsize = None
3261 self.chop_iterator = None
3262 self.closed = True
3264 def flush(self):
3265 pass
3268font_tab = {
3269 0: 'Helvetica',
3270 1: 'Helvetica-Bold',
3271}
3273font_tab_rev = dict((v, k) for (k, v) in font_tab.items())
3276class GMT(object):
3277 '''
3278 A thin wrapper to GMT command execution.
3280 A dict ``config`` may be given to override some of the default GMT
3281 parameters. The ``version`` argument may be used to select a specific GMT
3282 version, which should be used with this GMT instance. The selected
3283 version of GMT has to be installed on the system, must be supported by
3284 gmtpy and gmtpy must know where to find it.
3286 Each instance of this class is used for the task of producing one PS or PDF
3287 output file.
3289 Output of a series of GMT commands is accumulated in memory and can then be
3290 saved as PS or PDF file using the :py:meth:`save` method.
3292 GMT commands are accessed as method calls to instances of this class. See
3293 the :py:meth:`__getattr__` method for details on how the method's
3294 arguments are translated into options and arguments for the GMT command.
3296 Associated with each instance of this class, a temporary directory is
3297 created, where temporary files may be created, and which is automatically
3298 deleted, when the object is destroyed. The :py:meth:`tempfilename` method
3299 may be used to get a random filename in the instance's temporary directory.
3301 Any .gmtdefaults files are ignored. The GMT class uses a fixed
3302 set of defaults, which may be altered via an argument to the constructor.
3303 If possible, GMT is run in 'isolation mode', which was introduced with GMT
3304 version 4.2.2, by setting `GMT_TMPDIR` to the instance's temporary
3305 directory. With earlier versions of GMT, problems may arise with parallel
3306 execution of more than one GMT instance.
3308 Each instance of the GMT class may pick a specific version of GMT which
3309 shall be used, so that, if multiple versions of GMT are installed on the
3310 system, different versions of GMT can be used simultaneously such that
3311 backward compatibility of the scripts can be maintained.
3313 '''
3315 def __init__(
3316 self,
3317 config=None,
3318 kontinue=None,
3319 version='newest',
3320 config_papersize=None,
3321 eps_mode=False):
3323 self.installation = get_gmt_installation(version)
3324 self.gmt_config = dict(self.installation['defaults'])
3325 self.eps_mode = eps_mode
3326 self._shutil = shutil
3328 if config:
3329 self.gmt_config.update(config)
3331 if config_papersize:
3332 if not isinstance(config_papersize, str):
3333 config_papersize = 'Custom_%ix%i' % (
3334 int(config_papersize[0]), int(config_papersize[1]))
3336 if self.is_gmt5():
3337 self.gmt_config['PS_MEDIA'] = config_papersize
3338 else:
3339 self.gmt_config['PAPER_MEDIA'] = config_papersize
3341 self.tempdir = tempfile.mkdtemp("", "gmtpy-")
3342 self.gmt_config_filename = pjoin(self.tempdir, 'gmt.conf')
3343 self.gen_gmt_config_file(self.gmt_config_filename, self.gmt_config)
3345 if kontinue is not None:
3346 self.load_unfinished(kontinue)
3347 self.needstart = False
3348 else:
3349 self.output = BytesIO()
3350 self.needstart = True
3352 self.finished = False
3354 self.environ = os.environ.copy()
3355 self.environ['GMTHOME'] = self.installation.get('home', '')
3356 # GMT isolation mode: works only properly with GMT version >= 4.2.2
3357 self.environ['GMT_TMPDIR'] = self.tempdir
3359 self.layout = None
3360 self.command_log = []
3361 self.keep_temp_dir = False
3363 def is_gmt5(self):
3364 return self.installation['version'][0] in ['5', '6']
3366 def get_version(self):
3367 return self.installation['version']
3369 def get_config(self, key):
3370 return self.gmt_config[key]
3372 def to_points(self, string):
3373 if not string:
3374 return 0
3376 unit = string[-1]
3377 if unit in _units:
3378 return float(string[:-1])/_units[unit]
3379 else:
3380 default_unit = measure_unit(self.gmt_config).lower()[0]
3381 return float(string)/_units[default_unit]
3383 def label_font_size(self):
3384 if self.is_gmt5():
3385 return self.to_points(self.gmt_config['FONT_LABEL'].split(',')[0])
3386 else:
3387 return self.to_points(self.gmt_config['LABEL_FONT_SIZE'])
3389 def label_font(self):
3390 if self.is_gmt5():
3391 return font_tab_rev(self.gmt_config['FONT_LABEL'].split(',')[1])
3392 else:
3393 return self.gmt_config['LABEL_FONT']
3395 def gen_gmt_config_file(self, config_filename, config):
3396 f = open(config_filename, 'wb')
3397 f.write(
3398 ('#\n# GMT %s Defaults file\n'
3399 % self.installation['version']).encode('ascii'))
3401 for k, v in config.items():
3402 f.write(('%s = %s\n' % (k, v)).encode('ascii'))
3403 f.close()
3405 def __del__(self):
3406 if not self.keep_temp_dir:
3407 self._shutil.rmtree(self.tempdir)
3409 def _gmtcommand(self, command, *addargs, **kwargs):
3411 '''
3412 Execute arbitrary GMT command.
3414 See docstring in __getattr__ for details.
3415 '''
3417 in_stream = kwargs.pop('in_stream', None)
3418 in_filename = kwargs.pop('in_filename', None)
3419 in_string = kwargs.pop('in_string', None)
3420 in_columns = kwargs.pop('in_columns', None)
3421 in_rows = kwargs.pop('in_rows', None)
3422 out_stream = kwargs.pop('out_stream', None)
3423 out_filename = kwargs.pop('out_filename', None)
3424 out_discard = kwargs.pop('out_discard', None)
3425 finish = kwargs.pop('finish', False)
3426 suppressdefaults = kwargs.pop('suppress_defaults', False)
3427 config_override = kwargs.pop('config', None)
3429 assert(not self.finished)
3431 # check for mutual exclusiveness on input and output possibilities
3432 assert(1 >= len(
3433 [x for x in [
3434 in_stream, in_filename, in_string, in_columns, in_rows]
3435 if x is not None]))
3436 assert(1 >= len([x for x in [out_stream, out_filename, out_discard]
3437 if x is not None]))
3439 options = []
3441 gmt_config = self.gmt_config
3442 if not self.is_gmt5():
3443 gmt_config_filename = self.gmt_config_filename
3444 if config_override:
3445 gmt_config = self.gmt_config.copy()
3446 gmt_config.update(config_override)
3447 gmt_config_override_filename = pjoin(
3448 self.tempdir, 'gmtdefaults_override')
3449 self.gen_gmt_config_file(
3450 gmt_config_override_filename, gmt_config)
3451 gmt_config_filename = gmt_config_override_filename
3453 else: # gmt5 needs override variables as --VAR=value
3454 if config_override:
3455 for k, v in config_override.items():
3456 options.append('--%s=%s' % (k, v))
3458 if out_discard:
3459 out_filename = '/dev/null'
3461 out_mustclose = False
3462 if out_filename is not None:
3463 out_mustclose = True
3464 out_stream = open(out_filename, 'wb')
3466 if in_filename is not None:
3467 in_stream = open(in_filename, 'rb')
3469 if in_string is not None:
3470 in_stream = BytesIO(in_string)
3472 encoding_gmt = gmt_config.get(
3473 'PS_CHAR_ENCODING',
3474 gmt_config.get('CHAR_ENCODING', 'ISOLatin1+'))
3476 encoding = encoding_gmt_to_python[encoding_gmt.lower()]
3478 if in_columns is not None or in_rows is not None:
3479 in_stream = LineStreamChopper(TableLiner(in_columns=in_columns,
3480 in_rows=in_rows,
3481 encoding=encoding))
3483 # convert option arguments to strings
3484 for k, v in kwargs.items():
3485 if len(k) > 1:
3486 raise GmtPyError('Found illegal keyword argument "%s" '
3487 'while preparing options for command "%s"'
3488 % (k, command))
3490 if type(v) is bool:
3491 if v:
3492 options.append('-%s' % k)
3493 elif type(v) is tuple or type(v) is list:
3494 options.append('-%s' % k + '/'.join([str(x) for x in v]))
3495 else:
3496 options.append('-%s%s' % (k, str(v)))
3498 # if not redirecting to an external sink, handle -K -O
3499 if out_stream is None:
3500 if not finish:
3501 options.append('-K')
3502 else:
3503 self.finished = True
3505 if not self.needstart:
3506 options.append('-O')
3507 else:
3508 self.needstart = False
3510 out_stream = self.output
3512 # run the command
3513 if self.is_gmt5():
3514 args = [pjoin(self.installation['bin'], 'gmt'), command]
3515 else:
3516 args = [pjoin(self.installation['bin'], command)]
3518 if not os.path.isfile(args[0]):
3519 raise OSError('No such file: %s' % args[0])
3520 args.extend(options)
3521 args.extend(addargs)
3522 if not self.is_gmt5() and not suppressdefaults:
3523 # does not seem to work with GMT 5 (and should not be necessary
3524 args.append('+'+gmt_config_filename)
3526 bs = 2048
3527 p = subprocess.Popen(args, stdin=subprocess.PIPE,
3528 stdout=subprocess.PIPE, bufsize=bs,
3529 env=self.environ)
3530 while True:
3531 cr, cw, cx = select([p.stdout], [p.stdin], [])
3532 if cr:
3533 out_stream.write(p.stdout.read(bs))
3534 if cw:
3535 if in_stream is not None:
3536 data = in_stream.read(bs)
3537 if len(data) == 0:
3538 break
3539 p.stdin.write(data)
3540 else:
3541 break
3542 if not cr and not cw:
3543 break
3545 p.stdin.close()
3547 while True:
3548 data = p.stdout.read(bs)
3549 if len(data) == 0:
3550 break
3551 out_stream.write(data)
3553 p.stdout.close()
3555 retcode = p.wait()
3557 if in_stream is not None:
3558 in_stream.close()
3560 if out_mustclose:
3561 out_stream.close()
3563 if retcode != 0:
3564 self.keep_temp_dir = True
3565 raise GMTError('Command %s returned an error. '
3566 'While executing command:\n%s'
3567 % (command, escape_shell_args(args)))
3569 self.command_log.append(args)
3571 def __getattr__(self, command):
3573 '''
3574 Maps to call self._gmtcommand(command, \\*addargs, \\*\\*kwargs).
3576 Execute arbitrary GMT command.
3578 Run a GMT command and by default append its postscript output to the
3579 output file maintained by the GMT instance on which this method is
3580 called.
3582 Except for a few keyword arguments listed below, any ``kwargs`` and
3583 ``addargs`` are converted into command line options and arguments and
3584 passed to the GMT command. Numbers in keyword arguments are converted
3585 into strings. E.g. ``S=10`` is translated into ``'-S10'``. Tuples of
3586 numbers or strings are converted into strings where the elements of the
3587 tuples are separated by slashes '/'. E.g. ``R=(10, 10, 20, 20)`` is
3588 translated into ``'-R10/10/20/20'``. Options with a boolean argument
3589 are only appended to the GMT command, if their values are True.
3591 If no output redirection is in effect, the -K and -O options are
3592 handled by gmtpy and thus should not be specified. Use
3593 ``out_discard=True`` if you don't want -K or -O beeing added, but are
3594 not interested in the output.
3596 The standard input of the GMT process is fed by data selected with one
3597 of the following ``in_*`` keyword arguments:
3599 =============== =======================================================
3600 ``in_stream`` Data is read from an open file like object.
3601 ``in_filename`` Data is read from the given file.
3602 ``in_string`` String content is dumped to the process.
3603 ``in_columns`` A 2D nested iterable whose elements can be accessed as
3604 ``in_columns[icolumn][irow]`` is converted into an
3605 ascii
3606 table, which is fed to the process.
3607 ``in_rows`` A 2D nested iterable whos elements can be accessed as
3608 ``in_rows[irow][icolumn]`` is converted into an ascii
3609 table, which is fed to the process.
3610 =============== =======================================================
3612 The standard output of the GMT process may be redirected by one of the
3613 following options:
3615 ================= =====================================================
3616 ``out_stream`` Output is fed to an open file like object.
3617 ``out_filename`` Output is dumped to the given file.
3618 ``out_discard`` If True, output is dumped to :file:`/dev/null`.
3619 ================= =====================================================
3621 Additional keyword arguments:
3623 ===================== =================================================
3624 ``config`` Dict with GMT defaults which override the
3625 currently active set of defaults exclusively
3626 during this call.
3627 ``finish`` If True, the postscript file, which is maintained
3628 by the GMT instance is finished, and no further
3629 plotting is allowed.
3630 ``suppress_defaults`` Suppress appending of the ``'+gmtdefaults'``
3631 option to the command.
3632 ===================== =================================================
3634 '''
3636 def f(*args, **kwargs):
3637 return self._gmtcommand(command, *args, **kwargs)
3638 return f
3640 def tempfilename(self, name=None):
3641 '''
3642 Get filename for temporary file in the private temp directory.
3644 If no ``name`` argument is given, a random name is picked. If
3645 ``name`` is given, returns a path ending in that ``name``.
3646 '''
3648 if not name:
3649 name = ''.join(
3650 [random.choice('abcdefghijklmnopqrstuvwxyz')
3651 for i in range(10)])
3653 fn = pjoin(self.tempdir, name)
3654 return fn
3656 def tempfile(self, name=None):
3657 '''
3658 Create and open a file in the private temp directory.
3659 '''
3661 fn = self.tempfilename(name)
3662 f = open(fn, 'wb')
3663 return f, fn
3665 def save_unfinished(self, filename):
3666 out = open(filename, 'wb')
3667 out.write(self.output.getvalue())
3668 out.close()
3670 def load_unfinished(self, filename):
3671 self.output = BytesIO()
3672 self.finished = False
3673 inp = open(filename, 'rb')
3674 self.output.write(inp.read())
3675 inp.close()
3677 def dump(self, ident):
3678 filename = self.tempfilename('breakpoint-%s' % ident)
3679 self.save_unfinished(filename)
3681 def load(self, ident):
3682 filename = self.tempfilename('breakpoint-%s' % ident)
3683 self.load_unfinished(filename)
3685 def save(self, filename=None, bbox=None, resolution=150, oversample=2.,
3686 width=None, height=None, size=None, crop_eps_mode=False,
3687 psconvert=False):
3689 '''
3690 Finish and save figure as PDF, PS or PPM file.
3692 If filename ends with ``'.pdf'`` a PDF file is created by piping the
3693 GMT output through :program:`gmtpy-epstopdf`.
3695 If filename ends with ``'.png'`` a PNG file is created by running
3696 :program:`gmtpy-epstopdf`, :program:`pdftocairo` and
3697 :program:`convert`. ``resolution`` specifies the resolution in DPI for
3698 raster file formats. Rasterization is done at a higher resolution if
3699 ``oversample`` is set to a value higher than one. The output image size
3700 can also be controlled by setting ``width``, ``height`` or ``size``
3701 instead of ``resolution``. When ``size`` is given, the image is scaled
3702 so that ``max(width, height) == size``.
3704 The bounding box is set according to the values given in ``bbox``.
3705 '''
3707 if not self.finished:
3708 self.psxy(R=True, J=True, finish=True)
3710 if filename:
3711 tempfn = pjoin(self.tempdir, 'incomplete')
3712 out = open(tempfn, 'wb')
3713 else:
3714 out = sys.stdout
3716 if bbox and not self.is_gmt5():
3717 out.write(replace_bbox(bbox, self.output.getvalue()))
3718 else:
3719 out.write(self.output.getvalue())
3721 if filename:
3722 out.close()
3724 if filename.endswith('.ps') or (
3725 not self.is_gmt5() and filename.endswith('.eps')):
3727 shutil.move(tempfn, filename)
3728 return
3730 if self.is_gmt5():
3731 if crop_eps_mode:
3732 addarg = ['-A']
3733 else:
3734 addarg = []
3736 subprocess.call(
3737 [pjoin(self.installation['bin'], 'gmt'), 'psconvert',
3738 '-Te', '-F%s' % tempfn, tempfn, ] + addarg)
3740 if bbox:
3741 with open(tempfn + '.eps', 'rb') as fin:
3742 with open(tempfn + '-fixbb.eps', 'wb') as fout:
3743 replace_bbox(bbox, fin, fout)
3745 shutil.move(tempfn + '-fixbb.eps', tempfn + '.eps')
3747 else:
3748 shutil.move(tempfn, tempfn + '.eps')
3750 if filename.endswith('.eps'):
3751 shutil.move(tempfn + '.eps', filename)
3752 return
3754 elif filename.endswith('.pdf'):
3755 if psconvert:
3756 gmt_bin = pjoin(self.installation['bin'], 'gmt')
3757 subprocess.call([gmt_bin, 'psconvert', tempfn + '.eps', '-Tf',
3758 '-F' + filename])
3759 else:
3760 subprocess.call(['gmtpy-epstopdf', '--res=%i' % resolution,
3761 '--outfile=' + filename, tempfn + '.eps'])
3762 else:
3763 subprocess.call([
3764 'gmtpy-epstopdf',
3765 '--res=%i' % (resolution * oversample),
3766 '--outfile=' + tempfn + '.pdf', tempfn + '.eps'])
3768 convert_graph(
3769 tempfn + '.pdf', filename,
3770 resolution=resolution, oversample=oversample,
3771 size=size, width=width, height=height)
3773 def bbox(self):
3774 return get_bbox(self.output.getvalue())
3776 def get_command_log(self):
3777 '''
3778 Get the command log.
3779 '''
3781 return self.command_log
3783 def __str__(self):
3784 s = ''
3785 for com in self.command_log:
3786 s += com[0] + "\n " + "\n ".join(com[1:]) + "\n\n"
3787 return s
3789 def page_size_points(self):
3790 '''
3791 Try to get paper size of output postscript file in points.
3792 '''
3794 pm = paper_media(self.gmt_config).lower()
3795 if pm.endswith('+') or pm.endswith('-'):
3796 pm = pm[:-1]
3798 orient = page_orientation(self.gmt_config).lower()
3800 if pm in all_paper_sizes():
3802 if orient == 'portrait':
3803 return get_paper_size(pm)
3804 else:
3805 return get_paper_size(pm)[1], get_paper_size(pm)[0]
3807 m = re.match(r'custom_([0-9.]+)([cimp]?)x([0-9.]+)([cimp]?)', pm)
3808 if m:
3809 w, uw, h, uh = m.groups()
3810 w, h = float(w), float(h)
3811 if uw:
3812 w *= _units[uw]
3813 if uh:
3814 h *= _units[uh]
3815 if orient == 'portrait':
3816 return w, h
3817 else:
3818 return h, w
3820 return None, None
3822 def default_layout(self, with_palette=False):
3823 '''
3824 Get a default layout for the output page.
3826 One of three different layouts is choosen, depending on the
3827 `PAPER_MEDIA` setting in the GMT configuration dict.
3829 If `PAPER_MEDIA` ends with a ``'+'`` (EPS output is selected), a
3830 :py:class:`FrameLayout` is centered on the page, whose size is
3831 controlled by its center widget's size plus the margins of the
3832 :py:class:`FrameLayout`.
3834 If `PAPER_MEDIA` indicates, that a custom page size is wanted by
3835 starting with ``'Custom_'``, a :py:class:`FrameLayout` is used to fill
3836 the complete page. The center widget's size is then controlled by the
3837 page's size minus the margins of the :py:class:`FrameLayout`.
3839 In any other case, two FrameLayouts are nested, such that the outer
3840 layout attaches a 1 cm (printer) margin around the complete page, and
3841 the inner FrameLayout's center widget takes up as much space as
3842 possible under the constraint, that an aspect ratio of 1/golden_ratio
3843 is preserved.
3845 In any case, a reference to the innermost :py:class:`FrameLayout`
3846 instance is returned. The top-level layout can be accessed by calling
3847 :py:meth:`Widget.get_parent` on the returned layout.
3848 '''
3850 if self.layout is None:
3851 w, h = self.page_size_points()
3853 if w is None or h is None:
3854 raise GmtPyError("Can't determine page size for layout")
3856 pm = paper_media(self.gmt_config).lower()
3858 if with_palette:
3859 palette_layout = GridLayout(3, 1)
3860 spacer = palette_layout.get_widget(1, 0)
3861 palette_widget = palette_layout.get_widget(2, 0)
3862 spacer.set_horizontal(0.5*cm)
3863 palette_widget.set_horizontal(0.5*cm)
3865 if pm.endswith('+') or self.eps_mode:
3866 outer = CenterLayout()
3867 outer.set_policy((w, h), (0., 0.))
3868 inner = FrameLayout()
3869 outer.set_widget(inner)
3870 if with_palette:
3871 inner.set_widget('center', palette_layout)
3872 widget = palette_layout
3873 else:
3874 widget = inner.get_widget('center')
3875 widget.set_policy((w/golden_ratio, 0.), (0., 0.),
3876 aspect=1./golden_ratio)
3877 mw = 3.0*cm
3878 inner.set_fixed_margins(
3879 mw, mw, mw/golden_ratio, mw/golden_ratio)
3880 self.layout = inner
3882 elif pm.startswith('custom_'):
3883 layout = FrameLayout()
3884 layout.set_policy((w, h), (0., 0.))
3885 mw = 3.0*cm
3886 layout.set_min_margins(
3887 mw, mw, mw/golden_ratio, mw/golden_ratio)
3888 if with_palette:
3889 layout.set_widget('center', palette_layout)
3890 self.layout = layout
3891 else:
3892 outer = FrameLayout()
3893 outer.set_policy((w, h), (0., 0.))
3894 outer.set_fixed_margins(1.*cm, 1.*cm, 1.*cm, 1.*cm)
3896 inner = FrameLayout()
3897 outer.set_widget('center', inner)
3898 mw = 3.0*cm
3899 inner.set_min_margins(mw, mw, mw/golden_ratio, mw/golden_ratio)
3900 if with_palette:
3901 inner.set_widget('center', palette_layout)
3902 widget = palette_layout
3903 else:
3904 widget = inner.get_widget('center')
3906 widget.set_aspect(1./golden_ratio)
3908 self.layout = inner
3910 return self.layout
3912 def draw_layout(self, layout):
3913 '''
3914 Use psxy to draw layout; for debugging
3915 '''
3917 # corners = layout.get_corners(descend=True)
3918 rects = num.array(layout.get_sizes(), dtype=float)
3919 rects_wid = rects[:, 0, 0]
3920 rects_hei = rects[:, 0, 1]
3921 rects_center_x = rects[:, 1, 0] + rects_wid*0.5
3922 rects_center_y = rects[:, 1, 1] + rects_hei*0.5
3923 nrects = len(rects)
3924 prects = (rects_center_x, rects_center_y, num.arange(nrects),
3925 num.zeros(nrects), rects_hei, rects_wid)
3927 # points = num.array(corners, dtype=float)
3929 cptfile = self.tempfilename() + '.cpt'
3930 self.makecpt(
3931 C='ocean',
3932 T='%g/%g/%g' % (-nrects, nrects, 1),
3933 Z=True,
3934 out_filename=cptfile, suppress_defaults=True)
3936 bb = layout.bbox()
3937 self.psxy(
3938 in_columns=prects,
3939 C=cptfile,
3940 W='1p',
3941 S='J',
3942 R=(bb[0], bb[2], bb[1], bb[3]),
3943 *layout.XYJ())
3946def simpleconf_to_ax(conf, axname):
3947 c = {}
3948 x = axname
3949 for x in ('', axname):
3950 for k in ('label', 'unit', 'scaled_unit', 'scaled_unit_factor',
3951 'space', 'mode', 'approx_ticks', 'limits', 'masking', 'inc',
3952 'snap'):
3954 if x+k in conf:
3955 c[k] = conf[x+k]
3957 return Ax(**c)
3960class DensityPlotDef(object):
3961 def __init__(self, data, cpt='ocean', tension=0.7, size=(640, 480),
3962 contour=False, method='surface', zscaler=None, **extra):
3963 self.data = data
3964 self.cpt = cpt
3965 self.tension = tension
3966 self.size = size
3967 self.contour = contour
3968 self.method = method
3969 self.zscaler = zscaler
3970 self.extra = extra
3973class TextDef(object):
3974 def __init__(
3975 self,
3976 data,
3977 size=9,
3978 justify='MC',
3979 fontno=0,
3980 offset=(0, 0),
3981 color='black'):
3983 self.data = data
3984 self.size = size
3985 self.justify = justify
3986 self.fontno = fontno
3987 self.offset = offset
3988 self.color = color
3991class Simple(object):
3992 def __init__(self, gmtconfig=None, gmtversion='newest', **simple_config):
3993 self.data = []
3994 self.symbols = []
3995 self.config = copy.deepcopy(simple_config)
3996 self.gmtconfig = gmtconfig
3997 self.density_plot_defs = []
3998 self.text_defs = []
4000 self.gmtversion = gmtversion
4002 self.data_x = []
4003 self.symbols_x = []
4005 self.data_y = []
4006 self.symbols_y = []
4008 self.default_config = {}
4009 self.set_defaults(width=15.*cm,
4010 height=15.*cm / golden_ratio,
4011 margins=(2.*cm, 2.*cm, 2.*cm, 2.*cm),
4012 with_palette=False,
4013 palette_offset=0.5*cm,
4014 palette_width=None,
4015 palette_height=None,
4016 zlabeloffset=2*cm,
4017 draw_layout=False)
4019 self.setup_defaults()
4020 self.fixate_widget_aspect = False
4022 def setup_defaults(self):
4023 pass
4025 def set_defaults(self, **kwargs):
4026 self.default_config.update(kwargs)
4028 def plot(self, data, symbol=''):
4029 self.data.append(data)
4030 self.symbols.append(symbol)
4032 def density_plot(self, data, **kwargs):
4033 dpd = DensityPlotDef(data, **kwargs)
4034 self.density_plot_defs.append(dpd)
4036 def text(self, data, **kwargs):
4037 dpd = TextDef(data, **kwargs)
4038 self.text_defs.append(dpd)
4040 def plot_x(self, data, symbol=''):
4041 self.data_x.append(data)
4042 self.symbols_x.append(symbol)
4044 def plot_y(self, data, symbol=''):
4045 self.data_y.append(data)
4046 self.symbols_y.append(symbol)
4048 def set(self, **kwargs):
4049 self.config.update(kwargs)
4051 def setup_base(self, conf):
4052 w = conf.pop('width')
4053 h = conf.pop('height')
4054 margins = conf.pop('margins')
4056 gmtconfig = {}
4057 if self.gmtconfig is not None:
4058 gmtconfig.update(self.gmtconfig)
4060 gmt = GMT(
4061 version=self.gmtversion,
4062 config=gmtconfig,
4063 config_papersize='Custom_%ix%i' % (w, h))
4065 layout = gmt.default_layout(with_palette=conf['with_palette'])
4066 layout.set_min_margins(*margins)
4067 if conf['with_palette']:
4068 widget = layout.get_widget().get_widget(0, 0)
4069 spacer = layout.get_widget().get_widget(1, 0)
4070 spacer.set_horizontal(conf['palette_offset'])
4071 palette_widget = layout.get_widget().get_widget(2, 0)
4072 if conf['palette_width'] is not None:
4073 palette_widget.set_horizontal(conf['palette_width'])
4074 if conf['palette_height'] is not None:
4075 palette_widget.set_vertical(conf['palette_height'])
4076 widget.set_vertical(h-margins[2]-margins[3]-0.03*cm)
4077 return gmt, layout, widget, palette_widget
4078 else:
4079 widget = layout.get_widget()
4080 return gmt, layout, widget, None
4082 def setup_projection(self, widget, scaler, conf):
4083 pass
4085 def setup_scaling(self, conf):
4086 ndims = 2
4087 if self.density_plot_defs:
4088 ndims = 3
4090 axes = [simpleconf_to_ax(conf, x) for x in 'xyz'[:ndims]]
4092 data_all = []
4093 data_all.extend(self.data)
4094 for dsd in self.density_plot_defs:
4095 if dsd.zscaler is None:
4096 data_all.append(dsd.data)
4097 else:
4098 data_all.append(dsd.data[:2])
4099 data_chopped = [ds[:ndims] for ds in data_all]
4101 scaler = ScaleGuru(data_chopped, axes=axes[:ndims])
4103 self.setup_scaling_plus(scaler, axes[:ndims])
4105 return scaler
4107 def setup_scaling_plus(self, scaler, axes):
4108 pass
4110 def setup_scaling_extra(self, scaler, conf):
4112 scaler_x = scaler.copy()
4113 scaler_x.data_ranges[1] = (0., 1.)
4114 scaler_x.axes[1].mode = 'off'
4116 scaler_y = scaler.copy()
4117 scaler_y.data_ranges[0] = (0., 1.)
4118 scaler_y.axes[0].mode = 'off'
4120 return scaler_x, scaler_y
4122 def draw_density(self, gmt, widget, scaler):
4124 R = scaler.R()
4125 # par = scaler.get_params()
4126 rxyj = R + widget.XYJ()
4127 innerticks = False
4128 for dpd in self.density_plot_defs:
4130 fn_cpt = gmt.tempfilename() + '.cpt'
4132 if dpd.zscaler is not None:
4133 s = dpd.zscaler
4134 else:
4135 s = scaler
4137 gmt.makecpt(C=dpd.cpt, out_filename=fn_cpt, *s.T())
4139 fn_grid = gmt.tempfilename()
4141 fn_mean = gmt.tempfilename()
4143 if dpd.method in ('surface', 'triangulate'):
4144 gmt.blockmean(in_columns=dpd.data,
4145 I='%i+/%i+' % dpd.size, # noqa
4146 out_filename=fn_mean, *R)
4148 if dpd.method == 'surface':
4149 gmt.surface(
4150 in_filename=fn_mean,
4151 T=dpd.tension,
4152 G=fn_grid,
4153 I='%i+/%i+' % dpd.size, # noqa
4154 out_discard=True,
4155 *R)
4157 if dpd.method == 'triangulate':
4158 gmt.triangulate(
4159 in_filename=fn_mean,
4160 G=fn_grid,
4161 I='%i+/%i+' % dpd.size, # noqa
4162 out_discard=True,
4163 V=True,
4164 *R)
4166 if gmt.is_gmt5():
4167 gmt.grdimage(fn_grid, C=fn_cpt, E='i', n='l', *rxyj)
4169 else:
4170 gmt.grdimage(fn_grid, C=fn_cpt, E='i', S='l', *rxyj)
4172 if dpd.contour:
4173 gmt.grdcontour(fn_grid, C=fn_cpt, W='0.5p,black', *rxyj)
4174 innerticks = '0.5p,black'
4176 os.remove(fn_grid)
4177 os.remove(fn_mean)
4179 if dpd.method == 'fillcontour':
4180 extra = dict(C=fn_cpt)
4181 extra.update(dpd.extra)
4182 gmt.pscontour(in_columns=dpd.data,
4183 I=True, *rxyj, **extra) # noqa
4185 if dpd.method == 'contour':
4186 extra = dict(W='0.5p,black', C=fn_cpt)
4187 extra.update(dpd.extra)
4188 gmt.pscontour(in_columns=dpd.data, *rxyj, **extra)
4190 return fn_cpt, innerticks
4192 def draw_basemap(self, gmt, widget, scaler):
4193 gmt.psbasemap(*(widget.JXY() + scaler.RB(ax_projection=True)))
4195 def draw(self, gmt, widget, scaler):
4196 rxyj = scaler.R() + widget.JXY()
4197 for dat, sym in zip(self.data, self.symbols):
4198 gmt.psxy(in_columns=dat, *(sym.split()+rxyj))
4200 def post_draw(self, gmt, widget, scaler):
4201 pass
4203 def pre_draw(self, gmt, widget, scaler):
4204 pass
4206 def draw_extra(self, gmt, widget, scaler_x, scaler_y):
4208 for dat, sym in zip(self.data_x, self.symbols_x):
4209 gmt.psxy(in_columns=dat,
4210 *(sym.split() + scaler_x.R() + widget.JXY()))
4212 for dat, sym in zip(self.data_y, self.symbols_y):
4213 gmt.psxy(in_columns=dat,
4214 *(sym.split() + scaler_y.R() + widget.JXY()))
4216 def draw_text(self, gmt, widget, scaler):
4218 rxyj = scaler.R() + widget.JXY()
4219 for td in self.text_defs:
4220 x, y = td.data[0:2]
4221 text = td.data[-1]
4222 size = td.size
4223 angle = 0
4224 fontno = td.fontno
4225 justify = td.justify
4226 color = td.color
4227 if gmt.is_gmt5():
4228 gmt.pstext(
4229 in_rows=[(x, y, text)],
4230 F='+f%gp,%s,%s+a%g+j%s' % (
4231 size, fontno, color, angle, justify),
4232 D='%gp/%gp' % td.offset, *rxyj)
4233 else:
4234 gmt.pstext(
4235 in_rows=[(x, y, size, angle, fontno, justify, text)],
4236 D='%gp/%gp' % td.offset, *rxyj)
4238 def save(self, filename, resolution=150):
4240 conf = dict(self.default_config)
4241 conf.update(self.config)
4243 gmt, layout, widget, palette_widget = self.setup_base(conf)
4244 scaler = self.setup_scaling(conf)
4245 scaler_x, scaler_y = self.setup_scaling_extra(scaler, conf)
4247 self.setup_projection(widget, scaler, conf)
4248 if self.fixate_widget_aspect:
4249 aspect = aspect_for_projection(
4250 gmt.installation['version'], *(widget.J() + scaler.R()))
4252 widget.set_aspect(aspect)
4254 if conf['draw_layout']:
4255 gmt.draw_layout(layout)
4256 cptfile = None
4257 if self.density_plot_defs:
4258 cptfile, innerticks = self.draw_density(gmt, widget, scaler)
4259 self.pre_draw(gmt, widget, scaler)
4260 self.draw(gmt, widget, scaler)
4261 self.post_draw(gmt, widget, scaler)
4262 self.draw_extra(gmt, widget, scaler_x, scaler_y)
4263 self.draw_text(gmt, widget, scaler)
4264 self.draw_basemap(gmt, widget, scaler)
4266 if palette_widget and cptfile:
4267 nice_palette(gmt, palette_widget, scaler, cptfile,
4268 innerticks=innerticks,
4269 zlabeloffset=conf['zlabeloffset'])
4271 gmt.save(filename, resolution=resolution)
4274class LinLinPlot(Simple):
4275 pass
4278class LogLinPlot(Simple):
4280 def setup_defaults(self):
4281 self.set_defaults(xmode='min-max')
4283 def setup_projection(self, widget, scaler, conf):
4284 widget['J'] = '-JX%(width)gpl/%(height)gp'
4285 scaler['B'] = '-B2:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen'
4288class LinLogPlot(Simple):
4290 def setup_defaults(self):
4291 self.set_defaults(ymode='min-max')
4293 def setup_projection(self, widget, scaler, conf):
4294 widget['J'] = '-JX%(width)gp/%(height)gpl'
4295 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/2:%(ylabel)s:WSen'
4298class LogLogPlot(Simple):
4300 def setup_defaults(self):
4301 self.set_defaults(mode='min-max')
4303 def setup_projection(self, widget, scaler, conf):
4304 widget['J'] = '-JX%(width)gpl/%(height)gpl'
4305 scaler['B'] = '-B2:%(xlabel)s:/2:%(ylabel)s:WSen'
4308class AziDistPlot(Simple):
4310 def __init__(self, *args, **kwargs):
4311 Simple.__init__(self, *args, **kwargs)
4312 self.fixate_widget_aspect = True
4314 def setup_defaults(self):
4315 self.set_defaults(
4316 height=15.*cm,
4317 width=15.*cm,
4318 xmode='off',
4319 xlimits=(0., 360.),
4320 xinc=45.)
4322 def setup_projection(self, widget, scaler, conf):
4323 widget['J'] = '-JPa%(width)gp'
4325 def setup_scaling_plus(self, scaler, axes):
4326 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/%(yinc)g:%(ylabel)s:N'
4329class MPlot(Simple):
4331 def __init__(self, *args, **kwargs):
4332 Simple.__init__(self, *args, **kwargs)
4333 self.fixate_widget_aspect = True
4335 def setup_defaults(self):
4336 self.set_defaults(xmode='min-max', ymode='min-max')
4338 def setup_projection(self, widget, scaler, conf):
4339 par = scaler.get_params()
4340 lon0 = (par['xmin'] + par['xmax'])/2.
4341 lat0 = (par['ymin'] + par['ymax'])/2.
4342 sll = '%g/%g' % (lon0, lat0)
4343 widget['J'] = '-JM' + sll + '/%(width)gp'
4344 scaler['B'] = \
4345 '-B%(xinc)gg%(xinc)g:%(xlabel)s:/%(yinc)gg%(yinc)g:%(ylabel)s:WSen'
4348def nice_palette(gmt, widget, scaleguru, cptfile, zlabeloffset=0.8*inch,
4349 innerticks=True):
4351 par = scaleguru.get_params()
4352 par_ax = scaleguru.get_params(ax_projection=True)
4353 nz_palette = int(widget.height()/inch * 300)
4354 px = num.zeros(nz_palette*2)
4355 px[1::2] += 1
4356 pz = num.linspace(par['zmin'], par['zmax'], nz_palette).repeat(2)
4357 pdz = pz[2]-pz[0]
4358 palgrdfile = gmt.tempfilename()
4359 pal_r = (0, 1, par['zmin'], par['zmax'])
4360 pal_ax_r = (0, 1, par_ax['zmin'], par_ax['zmax'])
4361 gmt.xyz2grd(
4362 G=palgrdfile, R=pal_r,
4363 I=(1, pdz), in_columns=(px, pz, pz), # noqa
4364 out_discard=True)
4366 gmt.grdimage(palgrdfile, R=pal_r, C=cptfile, *widget.JXY())
4367 if isinstance(innerticks, str):
4368 tickpen = innerticks
4369 gmt.grdcontour(palgrdfile, W=tickpen, R=pal_r, C=cptfile,
4370 *widget.JXY())
4372 negpalwid = '%gp' % -widget.width()
4373 if not isinstance(innerticks, str) and innerticks:
4374 ticklen = negpalwid
4375 else:
4376 ticklen = '0p'
4378 TICK_LENGTH_PARAM = 'MAP_TICK_LENGTH' if gmt.is_gmt5() else 'TICK_LENGTH'
4379 gmt.psbasemap(
4380 R=pal_ax_r, B='4::/%(zinc)g::nsw' % par_ax,
4381 config={TICK_LENGTH_PARAM: ticklen},
4382 *widget.JXY())
4384 if innerticks:
4385 gmt.psbasemap(
4386 R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax,
4387 config={TICK_LENGTH_PARAM: '0p'},
4388 *widget.JXY())
4389 else:
4390 gmt.psbasemap(R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax, *widget.JXY())
4392 if par_ax['zlabel']:
4393 label_font = gmt.label_font()
4394 label_font_size = gmt.label_font_size()
4395 label_offset = zlabeloffset
4396 gmt.pstext(
4397 R=(0, 1, 0, 2), D="%gp/0p" % label_offset,
4398 N=True,
4399 in_rows=[(1, 1, label_font_size, -90, label_font, 'CB',
4400 par_ax['zlabel'])],
4401 *widget.JXY())