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 is_gmt6(version='newest'):
3140 return get_gmt_installation(version)['version'][0] in ['6']
3143def aspect_for_projection(gmtversion, *args, **kwargs):
3145 gmt = GMT(version=gmtversion, eps_mode=True)
3147 if gmt.is_gmt5():
3148 gmt.psbasemap('-B+gblack', finish=True, *args, **kwargs)
3149 fn = gmt.tempfilename('test.eps')
3150 gmt.save(fn, crop_eps_mode=True)
3151 with open(fn, 'rb') as f:
3152 s = f.read()
3154 l, b, r, t = get_bbox(s)
3155 else:
3156 gmt.psbasemap('-G0', finish=True, *args, **kwargs)
3157 l, b, r, t = gmt.bbox()
3159 return (t-b)/(r-l)
3162def text_box(
3163 text, font=0, font_size=12., angle=0, gmtversion='newest', **kwargs):
3165 gmt = GMT(version=gmtversion)
3166 if gmt.is_gmt5():
3167 row = [0, 0, text]
3168 farg = ['-F+f%gp,%s,%s+j%s' % (font_size, font, 'black', 'BL')]
3169 else:
3170 row = [0, 0, font_size, 0, font, 'BL', text]
3171 farg = []
3173 gmt.pstext(
3174 in_rows=[row],
3175 finish=True,
3176 R=(0, 1, 0, 1),
3177 J='x10p',
3178 N=True,
3179 *farg,
3180 **kwargs)
3182 fn = gmt.tempfilename() + '.ps'
3183 gmt.save(fn)
3185 (_, stderr) = subprocess.Popen(
3186 ['gs', '-q', '-dNOPAUSE', '-dBATCH', '-r720', '-sDEVICE=bbox', fn],
3187 stderr=subprocess.PIPE).communicate()
3189 dx, dy = None, None
3190 for line in stderr.splitlines():
3191 if line.startswith(b'%%HiResBoundingBox:'):
3192 l, b, r, t = [float(x) for x in line.split()[-4:]]
3193 dx, dy = r-l, t-b
3194 break
3196 return dx, dy
3199class TableLiner(object):
3200 '''
3201 Utility class to turn tables into lines.
3202 '''
3204 def __init__(self, in_columns=None, in_rows=None, encoding='utf-8'):
3205 self.in_columns = in_columns
3206 self.in_rows = in_rows
3207 self.encoding = encoding
3209 def __iter__(self):
3210 if self.in_columns is not None:
3211 for row in zip(*self.in_columns):
3212 yield (' '.join([newstr(x) for x in row])+'\n').encode(
3213 self.encoding)
3215 if self.in_rows is not None:
3216 for row in self.in_rows:
3217 yield (' '.join([newstr(x) for x in row])+'\n').encode(
3218 self.encoding)
3221class LineStreamChopper(object):
3222 '''
3223 File-like object to buffer data.
3224 '''
3226 def __init__(self, liner):
3227 self.chopsize = None
3228 self.liner = liner
3229 self.chop_iterator = None
3230 self.closed = False
3232 def _chopiter(self):
3233 buf = BytesIO()
3234 for line in self.liner:
3235 buf.write(line)
3236 buflen = buf.tell()
3237 if self.chopsize is not None and buflen >= self.chopsize:
3238 buf.seek(0)
3239 while buf.tell() <= buflen-self.chopsize:
3240 yield buf.read(self.chopsize)
3242 newbuf = BytesIO()
3243 newbuf.write(buf.read())
3244 buf.close()
3245 buf = newbuf
3247 yield(buf.getvalue())
3248 buf.close()
3250 def read(self, size=None):
3251 if self.closed:
3252 raise ValueError('Cannot read from closed LineStreamChopper.')
3253 if self.chop_iterator is None:
3254 self.chopsize = size
3255 self.chop_iterator = self._chopiter()
3257 self.chopsize = size
3258 try:
3259 return next(self.chop_iterator)
3260 except StopIteration:
3261 return ''
3263 def close(self):
3264 self.chopsize = None
3265 self.chop_iterator = None
3266 self.closed = True
3268 def flush(self):
3269 pass
3272font_tab = {
3273 0: 'Helvetica',
3274 1: 'Helvetica-Bold',
3275}
3277font_tab_rev = dict((v, k) for (k, v) in font_tab.items())
3280class GMT(object):
3281 '''
3282 A thin wrapper to GMT command execution.
3284 A dict ``config`` may be given to override some of the default GMT
3285 parameters. The ``version`` argument may be used to select a specific GMT
3286 version, which should be used with this GMT instance. The selected
3287 version of GMT has to be installed on the system, must be supported by
3288 gmtpy and gmtpy must know where to find it.
3290 Each instance of this class is used for the task of producing one PS or PDF
3291 output file.
3293 Output of a series of GMT commands is accumulated in memory and can then be
3294 saved as PS or PDF file using the :py:meth:`save` method.
3296 GMT commands are accessed as method calls to instances of this class. See
3297 the :py:meth:`__getattr__` method for details on how the method's
3298 arguments are translated into options and arguments for the GMT command.
3300 Associated with each instance of this class, a temporary directory is
3301 created, where temporary files may be created, and which is automatically
3302 deleted, when the object is destroyed. The :py:meth:`tempfilename` method
3303 may be used to get a random filename in the instance's temporary directory.
3305 Any .gmtdefaults files are ignored. The GMT class uses a fixed
3306 set of defaults, which may be altered via an argument to the constructor.
3307 If possible, GMT is run in 'isolation mode', which was introduced with GMT
3308 version 4.2.2, by setting `GMT_TMPDIR` to the instance's temporary
3309 directory. With earlier versions of GMT, problems may arise with parallel
3310 execution of more than one GMT instance.
3312 Each instance of the GMT class may pick a specific version of GMT which
3313 shall be used, so that, if multiple versions of GMT are installed on the
3314 system, different versions of GMT can be used simultaneously such that
3315 backward compatibility of the scripts can be maintained.
3317 '''
3319 def __init__(
3320 self,
3321 config=None,
3322 kontinue=None,
3323 version='newest',
3324 config_papersize=None,
3325 eps_mode=False):
3327 self.installation = get_gmt_installation(version)
3328 self.gmt_config = dict(self.installation['defaults'])
3329 self.eps_mode = eps_mode
3330 self._shutil = shutil
3332 if config:
3333 self.gmt_config.update(config)
3335 if config_papersize:
3336 if not isinstance(config_papersize, str):
3337 config_papersize = 'Custom_%ix%i' % (
3338 int(config_papersize[0]), int(config_papersize[1]))
3340 if self.is_gmt5():
3341 self.gmt_config['PS_MEDIA'] = config_papersize
3342 else:
3343 self.gmt_config['PAPER_MEDIA'] = config_papersize
3345 self.tempdir = tempfile.mkdtemp("", "gmtpy-")
3346 self.gmt_config_filename = pjoin(self.tempdir, 'gmt.conf')
3347 self.gen_gmt_config_file(self.gmt_config_filename, self.gmt_config)
3349 if kontinue is not None:
3350 self.load_unfinished(kontinue)
3351 self.needstart = False
3352 else:
3353 self.output = BytesIO()
3354 self.needstart = True
3356 self.finished = False
3358 self.environ = os.environ.copy()
3359 self.environ['GMTHOME'] = self.installation.get('home', '')
3360 # GMT isolation mode: works only properly with GMT version >= 4.2.2
3361 self.environ['GMT_TMPDIR'] = self.tempdir
3363 self.layout = None
3364 self.command_log = []
3365 self.keep_temp_dir = False
3367 def is_gmt5(self):
3368 return self.get_version()[0] in ['5', '6']
3370 def is_gmt6(self):
3371 return self.get_version()[0] in ['6']
3373 def get_version(self):
3374 return self.installation['version']
3376 def get_config(self, key):
3377 return self.gmt_config[key]
3379 def to_points(self, string):
3380 if not string:
3381 return 0
3383 unit = string[-1]
3384 if unit in _units:
3385 return float(string[:-1])/_units[unit]
3386 else:
3387 default_unit = measure_unit(self.gmt_config).lower()[0]
3388 return float(string)/_units[default_unit]
3390 def label_font_size(self):
3391 if self.is_gmt5():
3392 return self.to_points(self.gmt_config['FONT_LABEL'].split(',')[0])
3393 else:
3394 return self.to_points(self.gmt_config['LABEL_FONT_SIZE'])
3396 def label_font(self):
3397 if self.is_gmt5():
3398 return font_tab_rev(self.gmt_config['FONT_LABEL'].split(',')[1])
3399 else:
3400 return self.gmt_config['LABEL_FONT']
3402 def gen_gmt_config_file(self, config_filename, config):
3403 f = open(config_filename, 'wb')
3404 f.write(
3405 ('#\n# GMT %s Defaults file\n'
3406 % self.installation['version']).encode('ascii'))
3408 for k, v in config.items():
3409 f.write(('%s = %s\n' % (k, v)).encode('ascii'))
3410 f.close()
3412 def __del__(self):
3413 if not self.keep_temp_dir:
3414 self._shutil.rmtree(self.tempdir)
3416 def _gmtcommand(self, command, *addargs, **kwargs):
3418 '''
3419 Execute arbitrary GMT command.
3421 See docstring in __getattr__ for details.
3422 '''
3424 in_stream = kwargs.pop('in_stream', None)
3425 in_filename = kwargs.pop('in_filename', None)
3426 in_string = kwargs.pop('in_string', None)
3427 in_columns = kwargs.pop('in_columns', None)
3428 in_rows = kwargs.pop('in_rows', None)
3429 out_stream = kwargs.pop('out_stream', None)
3430 out_filename = kwargs.pop('out_filename', None)
3431 out_discard = kwargs.pop('out_discard', None)
3432 finish = kwargs.pop('finish', False)
3433 suppressdefaults = kwargs.pop('suppress_defaults', False)
3434 config_override = kwargs.pop('config', None)
3436 assert(not self.finished)
3438 # check for mutual exclusiveness on input and output possibilities
3439 assert(1 >= len(
3440 [x for x in [
3441 in_stream, in_filename, in_string, in_columns, in_rows]
3442 if x is not None]))
3443 assert(1 >= len([x for x in [out_stream, out_filename, out_discard]
3444 if x is not None]))
3446 options = []
3448 gmt_config = self.gmt_config
3449 if not self.is_gmt5():
3450 gmt_config_filename = self.gmt_config_filename
3451 if config_override:
3452 gmt_config = self.gmt_config.copy()
3453 gmt_config.update(config_override)
3454 gmt_config_override_filename = pjoin(
3455 self.tempdir, 'gmtdefaults_override')
3456 self.gen_gmt_config_file(
3457 gmt_config_override_filename, gmt_config)
3458 gmt_config_filename = gmt_config_override_filename
3460 else: # gmt5 needs override variables as --VAR=value
3461 if config_override:
3462 for k, v in config_override.items():
3463 options.append('--%s=%s' % (k, v))
3465 if out_discard:
3466 out_filename = '/dev/null'
3468 out_mustclose = False
3469 if out_filename is not None:
3470 out_mustclose = True
3471 out_stream = open(out_filename, 'wb')
3473 if in_filename is not None:
3474 in_stream = open(in_filename, 'rb')
3476 if in_string is not None:
3477 in_stream = BytesIO(in_string)
3479 encoding_gmt = gmt_config.get(
3480 'PS_CHAR_ENCODING',
3481 gmt_config.get('CHAR_ENCODING', 'ISOLatin1+'))
3483 encoding = encoding_gmt_to_python[encoding_gmt.lower()]
3485 if in_columns is not None or in_rows is not None:
3486 in_stream = LineStreamChopper(TableLiner(in_columns=in_columns,
3487 in_rows=in_rows,
3488 encoding=encoding))
3490 # convert option arguments to strings
3491 for k, v in kwargs.items():
3492 if len(k) > 1:
3493 raise GmtPyError('Found illegal keyword argument "%s" '
3494 'while preparing options for command "%s"'
3495 % (k, command))
3497 if type(v) is bool:
3498 if v:
3499 options.append('-%s' % k)
3500 elif type(v) is tuple or type(v) is list:
3501 options.append('-%s' % k + '/'.join([str(x) for x in v]))
3502 else:
3503 options.append('-%s%s' % (k, str(v)))
3505 # if not redirecting to an external sink, handle -K -O
3506 if out_stream is None:
3507 if not finish:
3508 options.append('-K')
3509 else:
3510 self.finished = True
3512 if not self.needstart:
3513 options.append('-O')
3514 else:
3515 self.needstart = False
3517 out_stream = self.output
3519 # run the command
3520 if self.is_gmt5():
3521 args = [pjoin(self.installation['bin'], 'gmt'), command]
3522 else:
3523 args = [pjoin(self.installation['bin'], command)]
3525 if not os.path.isfile(args[0]):
3526 raise OSError('No such file: %s' % args[0])
3527 args.extend(options)
3528 args.extend(addargs)
3529 if not self.is_gmt5() and not suppressdefaults:
3530 # does not seem to work with GMT 5 (and should not be necessary
3531 args.append('+'+gmt_config_filename)
3533 bs = 2048
3534 p = subprocess.Popen(args, stdin=subprocess.PIPE,
3535 stdout=subprocess.PIPE, bufsize=bs,
3536 env=self.environ)
3537 while True:
3538 cr, cw, cx = select([p.stdout], [p.stdin], [])
3539 if cr:
3540 out_stream.write(p.stdout.read(bs))
3541 if cw:
3542 if in_stream is not None:
3543 data = in_stream.read(bs)
3544 if len(data) == 0:
3545 break
3546 p.stdin.write(data)
3547 else:
3548 break
3549 if not cr and not cw:
3550 break
3552 p.stdin.close()
3554 while True:
3555 data = p.stdout.read(bs)
3556 if len(data) == 0:
3557 break
3558 out_stream.write(data)
3560 p.stdout.close()
3562 retcode = p.wait()
3564 if in_stream is not None:
3565 in_stream.close()
3567 if out_mustclose:
3568 out_stream.close()
3570 if retcode != 0:
3571 self.keep_temp_dir = True
3572 raise GMTError('Command %s returned an error. '
3573 'While executing command:\n%s'
3574 % (command, escape_shell_args(args)))
3576 self.command_log.append(args)
3578 def __getattr__(self, command):
3580 '''
3581 Maps to call self._gmtcommand(command, \\*addargs, \\*\\*kwargs).
3583 Execute arbitrary GMT command.
3585 Run a GMT command and by default append its postscript output to the
3586 output file maintained by the GMT instance on which this method is
3587 called.
3589 Except for a few keyword arguments listed below, any ``kwargs`` and
3590 ``addargs`` are converted into command line options and arguments and
3591 passed to the GMT command. Numbers in keyword arguments are converted
3592 into strings. E.g. ``S=10`` is translated into ``'-S10'``. Tuples of
3593 numbers or strings are converted into strings where the elements of the
3594 tuples are separated by slashes '/'. E.g. ``R=(10, 10, 20, 20)`` is
3595 translated into ``'-R10/10/20/20'``. Options with a boolean argument
3596 are only appended to the GMT command, if their values are True.
3598 If no output redirection is in effect, the -K and -O options are
3599 handled by gmtpy and thus should not be specified. Use
3600 ``out_discard=True`` if you don't want -K or -O beeing added, but are
3601 not interested in the output.
3603 The standard input of the GMT process is fed by data selected with one
3604 of the following ``in_*`` keyword arguments:
3606 =============== =======================================================
3607 ``in_stream`` Data is read from an open file like object.
3608 ``in_filename`` Data is read from the given file.
3609 ``in_string`` String content is dumped to the process.
3610 ``in_columns`` A 2D nested iterable whose elements can be accessed as
3611 ``in_columns[icolumn][irow]`` is converted into an
3612 ascii
3613 table, which is fed to the process.
3614 ``in_rows`` A 2D nested iterable whos elements can be accessed as
3615 ``in_rows[irow][icolumn]`` is converted into an ascii
3616 table, which is fed to the process.
3617 =============== =======================================================
3619 The standard output of the GMT process may be redirected by one of the
3620 following options:
3622 ================= =====================================================
3623 ``out_stream`` Output is fed to an open file like object.
3624 ``out_filename`` Output is dumped to the given file.
3625 ``out_discard`` If True, output is dumped to :file:`/dev/null`.
3626 ================= =====================================================
3628 Additional keyword arguments:
3630 ===================== =================================================
3631 ``config`` Dict with GMT defaults which override the
3632 currently active set of defaults exclusively
3633 during this call.
3634 ``finish`` If True, the postscript file, which is maintained
3635 by the GMT instance is finished, and no further
3636 plotting is allowed.
3637 ``suppress_defaults`` Suppress appending of the ``'+gmtdefaults'``
3638 option to the command.
3639 ===================== =================================================
3641 '''
3643 def f(*args, **kwargs):
3644 return self._gmtcommand(command, *args, **kwargs)
3645 return f
3647 def tempfilename(self, name=None):
3648 '''
3649 Get filename for temporary file in the private temp directory.
3651 If no ``name`` argument is given, a random name is picked. If
3652 ``name`` is given, returns a path ending in that ``name``.
3653 '''
3655 if not name:
3656 name = ''.join(
3657 [random.choice('abcdefghijklmnopqrstuvwxyz')
3658 for i in range(10)])
3660 fn = pjoin(self.tempdir, name)
3661 return fn
3663 def tempfile(self, name=None):
3664 '''
3665 Create and open a file in the private temp directory.
3666 '''
3668 fn = self.tempfilename(name)
3669 f = open(fn, 'wb')
3670 return f, fn
3672 def save_unfinished(self, filename):
3673 out = open(filename, 'wb')
3674 out.write(self.output.getvalue())
3675 out.close()
3677 def load_unfinished(self, filename):
3678 self.output = BytesIO()
3679 self.finished = False
3680 inp = open(filename, 'rb')
3681 self.output.write(inp.read())
3682 inp.close()
3684 def dump(self, ident):
3685 filename = self.tempfilename('breakpoint-%s' % ident)
3686 self.save_unfinished(filename)
3688 def load(self, ident):
3689 filename = self.tempfilename('breakpoint-%s' % ident)
3690 self.load_unfinished(filename)
3692 def save(self, filename=None, bbox=None, resolution=150, oversample=2.,
3693 width=None, height=None, size=None, crop_eps_mode=False,
3694 psconvert=False):
3696 '''
3697 Finish and save figure as PDF, PS or PPM file.
3699 If filename ends with ``'.pdf'`` a PDF file is created by piping the
3700 GMT output through :program:`gmtpy-epstopdf`.
3702 If filename ends with ``'.png'`` a PNG file is created by running
3703 :program:`gmtpy-epstopdf`, :program:`pdftocairo` and
3704 :program:`convert`. ``resolution`` specifies the resolution in DPI for
3705 raster file formats. Rasterization is done at a higher resolution if
3706 ``oversample`` is set to a value higher than one. The output image size
3707 can also be controlled by setting ``width``, ``height`` or ``size``
3708 instead of ``resolution``. When ``size`` is given, the image is scaled
3709 so that ``max(width, height) == size``.
3711 The bounding box is set according to the values given in ``bbox``.
3712 '''
3714 if not self.finished:
3715 self.psxy(R=True, J=True, finish=True)
3717 if filename:
3718 tempfn = pjoin(self.tempdir, 'incomplete')
3719 out = open(tempfn, 'wb')
3720 else:
3721 out = sys.stdout
3723 if bbox and not self.is_gmt5():
3724 out.write(replace_bbox(bbox, self.output.getvalue()))
3725 else:
3726 out.write(self.output.getvalue())
3728 if filename:
3729 out.close()
3731 if filename.endswith('.ps') or (
3732 not self.is_gmt5() and filename.endswith('.eps')):
3734 shutil.move(tempfn, filename)
3735 return
3737 if self.is_gmt5():
3738 if crop_eps_mode:
3739 addarg = ['-A']
3740 else:
3741 addarg = []
3743 subprocess.call(
3744 [pjoin(self.installation['bin'], 'gmt'), 'psconvert',
3745 '-Te', '-F%s' % tempfn, tempfn, ] + addarg)
3747 if bbox:
3748 with open(tempfn + '.eps', 'rb') as fin:
3749 with open(tempfn + '-fixbb.eps', 'wb') as fout:
3750 replace_bbox(bbox, fin, fout)
3752 shutil.move(tempfn + '-fixbb.eps', tempfn + '.eps')
3754 else:
3755 shutil.move(tempfn, tempfn + '.eps')
3757 if filename.endswith('.eps'):
3758 shutil.move(tempfn + '.eps', filename)
3759 return
3761 elif filename.endswith('.pdf'):
3762 if psconvert:
3763 gmt_bin = pjoin(self.installation['bin'], 'gmt')
3764 subprocess.call([gmt_bin, 'psconvert', tempfn + '.eps', '-Tf',
3765 '-F' + filename])
3766 else:
3767 subprocess.call(['gmtpy-epstopdf', '--res=%i' % resolution,
3768 '--outfile=' + filename, tempfn + '.eps'])
3769 else:
3770 subprocess.call([
3771 'gmtpy-epstopdf',
3772 '--res=%i' % (resolution * oversample),
3773 '--outfile=' + tempfn + '.pdf', tempfn + '.eps'])
3775 convert_graph(
3776 tempfn + '.pdf', filename,
3777 resolution=resolution, oversample=oversample,
3778 size=size, width=width, height=height)
3780 def bbox(self):
3781 return get_bbox(self.output.getvalue())
3783 def get_command_log(self):
3784 '''
3785 Get the command log.
3786 '''
3788 return self.command_log
3790 def __str__(self):
3791 s = ''
3792 for com in self.command_log:
3793 s += com[0] + "\n " + "\n ".join(com[1:]) + "\n\n"
3794 return s
3796 def page_size_points(self):
3797 '''
3798 Try to get paper size of output postscript file in points.
3799 '''
3801 pm = paper_media(self.gmt_config).lower()
3802 if pm.endswith('+') or pm.endswith('-'):
3803 pm = pm[:-1]
3805 orient = page_orientation(self.gmt_config).lower()
3807 if pm in all_paper_sizes():
3809 if orient == 'portrait':
3810 return get_paper_size(pm)
3811 else:
3812 return get_paper_size(pm)[1], get_paper_size(pm)[0]
3814 m = re.match(r'custom_([0-9.]+)([cimp]?)x([0-9.]+)([cimp]?)', pm)
3815 if m:
3816 w, uw, h, uh = m.groups()
3817 w, h = float(w), float(h)
3818 if uw:
3819 w *= _units[uw]
3820 if uh:
3821 h *= _units[uh]
3822 if orient == 'portrait':
3823 return w, h
3824 else:
3825 return h, w
3827 return None, None
3829 def default_layout(self, with_palette=False):
3830 '''
3831 Get a default layout for the output page.
3833 One of three different layouts is choosen, depending on the
3834 `PAPER_MEDIA` setting in the GMT configuration dict.
3836 If `PAPER_MEDIA` ends with a ``'+'`` (EPS output is selected), a
3837 :py:class:`FrameLayout` is centered on the page, whose size is
3838 controlled by its center widget's size plus the margins of the
3839 :py:class:`FrameLayout`.
3841 If `PAPER_MEDIA` indicates, that a custom page size is wanted by
3842 starting with ``'Custom_'``, a :py:class:`FrameLayout` is used to fill
3843 the complete page. The center widget's size is then controlled by the
3844 page's size minus the margins of the :py:class:`FrameLayout`.
3846 In any other case, two FrameLayouts are nested, such that the outer
3847 layout attaches a 1 cm (printer) margin around the complete page, and
3848 the inner FrameLayout's center widget takes up as much space as
3849 possible under the constraint, that an aspect ratio of 1/golden_ratio
3850 is preserved.
3852 In any case, a reference to the innermost :py:class:`FrameLayout`
3853 instance is returned. The top-level layout can be accessed by calling
3854 :py:meth:`Widget.get_parent` on the returned layout.
3855 '''
3857 if self.layout is None:
3858 w, h = self.page_size_points()
3860 if w is None or h is None:
3861 raise GmtPyError("Can't determine page size for layout")
3863 pm = paper_media(self.gmt_config).lower()
3865 if with_palette:
3866 palette_layout = GridLayout(3, 1)
3867 spacer = palette_layout.get_widget(1, 0)
3868 palette_widget = palette_layout.get_widget(2, 0)
3869 spacer.set_horizontal(0.5*cm)
3870 palette_widget.set_horizontal(0.5*cm)
3872 if pm.endswith('+') or self.eps_mode:
3873 outer = CenterLayout()
3874 outer.set_policy((w, h), (0., 0.))
3875 inner = FrameLayout()
3876 outer.set_widget(inner)
3877 if with_palette:
3878 inner.set_widget('center', palette_layout)
3879 widget = palette_layout
3880 else:
3881 widget = inner.get_widget('center')
3882 widget.set_policy((w/golden_ratio, 0.), (0., 0.),
3883 aspect=1./golden_ratio)
3884 mw = 3.0*cm
3885 inner.set_fixed_margins(
3886 mw, mw, mw/golden_ratio, mw/golden_ratio)
3887 self.layout = inner
3889 elif pm.startswith('custom_'):
3890 layout = FrameLayout()
3891 layout.set_policy((w, h), (0., 0.))
3892 mw = 3.0*cm
3893 layout.set_min_margins(
3894 mw, mw, mw/golden_ratio, mw/golden_ratio)
3895 if with_palette:
3896 layout.set_widget('center', palette_layout)
3897 self.layout = layout
3898 else:
3899 outer = FrameLayout()
3900 outer.set_policy((w, h), (0., 0.))
3901 outer.set_fixed_margins(1.*cm, 1.*cm, 1.*cm, 1.*cm)
3903 inner = FrameLayout()
3904 outer.set_widget('center', inner)
3905 mw = 3.0*cm
3906 inner.set_min_margins(mw, mw, mw/golden_ratio, mw/golden_ratio)
3907 if with_palette:
3908 inner.set_widget('center', palette_layout)
3909 widget = palette_layout
3910 else:
3911 widget = inner.get_widget('center')
3913 widget.set_aspect(1./golden_ratio)
3915 self.layout = inner
3917 return self.layout
3919 def draw_layout(self, layout):
3920 '''
3921 Use psxy to draw layout; for debugging
3922 '''
3924 # corners = layout.get_corners(descend=True)
3925 rects = num.array(layout.get_sizes(), dtype=float)
3926 rects_wid = rects[:, 0, 0]
3927 rects_hei = rects[:, 0, 1]
3928 rects_center_x = rects[:, 1, 0] + rects_wid*0.5
3929 rects_center_y = rects[:, 1, 1] + rects_hei*0.5
3930 nrects = len(rects)
3931 prects = (rects_center_x, rects_center_y, num.arange(nrects),
3932 num.zeros(nrects), rects_hei, rects_wid)
3934 # points = num.array(corners, dtype=float)
3936 cptfile = self.tempfilename() + '.cpt'
3937 self.makecpt(
3938 C='ocean',
3939 T='%g/%g/%g' % (-nrects, nrects, 1),
3940 Z=True,
3941 out_filename=cptfile, suppress_defaults=True)
3943 bb = layout.bbox()
3944 self.psxy(
3945 in_columns=prects,
3946 C=cptfile,
3947 W='1p',
3948 S='J',
3949 R=(bb[0], bb[2], bb[1], bb[3]),
3950 *layout.XYJ())
3953def simpleconf_to_ax(conf, axname):
3954 c = {}
3955 x = axname
3956 for x in ('', axname):
3957 for k in ('label', 'unit', 'scaled_unit', 'scaled_unit_factor',
3958 'space', 'mode', 'approx_ticks', 'limits', 'masking', 'inc',
3959 'snap'):
3961 if x+k in conf:
3962 c[k] = conf[x+k]
3964 return Ax(**c)
3967class DensityPlotDef(object):
3968 def __init__(self, data, cpt='ocean', tension=0.7, size=(640, 480),
3969 contour=False, method='surface', zscaler=None, **extra):
3970 self.data = data
3971 self.cpt = cpt
3972 self.tension = tension
3973 self.size = size
3974 self.contour = contour
3975 self.method = method
3976 self.zscaler = zscaler
3977 self.extra = extra
3980class TextDef(object):
3981 def __init__(
3982 self,
3983 data,
3984 size=9,
3985 justify='MC',
3986 fontno=0,
3987 offset=(0, 0),
3988 color='black'):
3990 self.data = data
3991 self.size = size
3992 self.justify = justify
3993 self.fontno = fontno
3994 self.offset = offset
3995 self.color = color
3998class Simple(object):
3999 def __init__(self, gmtconfig=None, gmtversion='newest', **simple_config):
4000 self.data = []
4001 self.symbols = []
4002 self.config = copy.deepcopy(simple_config)
4003 self.gmtconfig = gmtconfig
4004 self.density_plot_defs = []
4005 self.text_defs = []
4007 self.gmtversion = gmtversion
4009 self.data_x = []
4010 self.symbols_x = []
4012 self.data_y = []
4013 self.symbols_y = []
4015 self.default_config = {}
4016 self.set_defaults(width=15.*cm,
4017 height=15.*cm / golden_ratio,
4018 margins=(2.*cm, 2.*cm, 2.*cm, 2.*cm),
4019 with_palette=False,
4020 palette_offset=0.5*cm,
4021 palette_width=None,
4022 palette_height=None,
4023 zlabeloffset=2*cm,
4024 draw_layout=False)
4026 self.setup_defaults()
4027 self.fixate_widget_aspect = False
4029 def setup_defaults(self):
4030 pass
4032 def set_defaults(self, **kwargs):
4033 self.default_config.update(kwargs)
4035 def plot(self, data, symbol=''):
4036 self.data.append(data)
4037 self.symbols.append(symbol)
4039 def density_plot(self, data, **kwargs):
4040 dpd = DensityPlotDef(data, **kwargs)
4041 self.density_plot_defs.append(dpd)
4043 def text(self, data, **kwargs):
4044 dpd = TextDef(data, **kwargs)
4045 self.text_defs.append(dpd)
4047 def plot_x(self, data, symbol=''):
4048 self.data_x.append(data)
4049 self.symbols_x.append(symbol)
4051 def plot_y(self, data, symbol=''):
4052 self.data_y.append(data)
4053 self.symbols_y.append(symbol)
4055 def set(self, **kwargs):
4056 self.config.update(kwargs)
4058 def setup_base(self, conf):
4059 w = conf.pop('width')
4060 h = conf.pop('height')
4061 margins = conf.pop('margins')
4063 gmtconfig = {}
4064 if self.gmtconfig is not None:
4065 gmtconfig.update(self.gmtconfig)
4067 gmt = GMT(
4068 version=self.gmtversion,
4069 config=gmtconfig,
4070 config_papersize='Custom_%ix%i' % (w, h))
4072 layout = gmt.default_layout(with_palette=conf['with_palette'])
4073 layout.set_min_margins(*margins)
4074 if conf['with_palette']:
4075 widget = layout.get_widget().get_widget(0, 0)
4076 spacer = layout.get_widget().get_widget(1, 0)
4077 spacer.set_horizontal(conf['palette_offset'])
4078 palette_widget = layout.get_widget().get_widget(2, 0)
4079 if conf['palette_width'] is not None:
4080 palette_widget.set_horizontal(conf['palette_width'])
4081 if conf['palette_height'] is not None:
4082 palette_widget.set_vertical(conf['palette_height'])
4083 widget.set_vertical(h-margins[2]-margins[3]-0.03*cm)
4084 return gmt, layout, widget, palette_widget
4085 else:
4086 widget = layout.get_widget()
4087 return gmt, layout, widget, None
4089 def setup_projection(self, widget, scaler, conf):
4090 pass
4092 def setup_scaling(self, conf):
4093 ndims = 2
4094 if self.density_plot_defs:
4095 ndims = 3
4097 axes = [simpleconf_to_ax(conf, x) for x in 'xyz'[:ndims]]
4099 data_all = []
4100 data_all.extend(self.data)
4101 for dsd in self.density_plot_defs:
4102 if dsd.zscaler is None:
4103 data_all.append(dsd.data)
4104 else:
4105 data_all.append(dsd.data[:2])
4106 data_chopped = [ds[:ndims] for ds in data_all]
4108 scaler = ScaleGuru(data_chopped, axes=axes[:ndims])
4110 self.setup_scaling_plus(scaler, axes[:ndims])
4112 return scaler
4114 def setup_scaling_plus(self, scaler, axes):
4115 pass
4117 def setup_scaling_extra(self, scaler, conf):
4119 scaler_x = scaler.copy()
4120 scaler_x.data_ranges[1] = (0., 1.)
4121 scaler_x.axes[1].mode = 'off'
4123 scaler_y = scaler.copy()
4124 scaler_y.data_ranges[0] = (0., 1.)
4125 scaler_y.axes[0].mode = 'off'
4127 return scaler_x, scaler_y
4129 def draw_density(self, gmt, widget, scaler):
4131 R = scaler.R()
4132 # par = scaler.get_params()
4133 rxyj = R + widget.XYJ()
4134 innerticks = False
4135 for dpd in self.density_plot_defs:
4137 fn_cpt = gmt.tempfilename() + '.cpt'
4139 if dpd.zscaler is not None:
4140 s = dpd.zscaler
4141 else:
4142 s = scaler
4144 gmt.makecpt(C=dpd.cpt, out_filename=fn_cpt, *s.T())
4146 fn_grid = gmt.tempfilename()
4148 fn_mean = gmt.tempfilename()
4150 if dpd.method in ('surface', 'triangulate'):
4151 gmt.blockmean(in_columns=dpd.data,
4152 I='%i+/%i+' % dpd.size, # noqa
4153 out_filename=fn_mean, *R)
4155 if dpd.method == 'surface':
4156 gmt.surface(
4157 in_filename=fn_mean,
4158 T=dpd.tension,
4159 G=fn_grid,
4160 I='%i+/%i+' % dpd.size, # noqa
4161 out_discard=True,
4162 *R)
4164 if dpd.method == 'triangulate':
4165 gmt.triangulate(
4166 in_filename=fn_mean,
4167 G=fn_grid,
4168 I='%i+/%i+' % dpd.size, # noqa
4169 out_discard=True,
4170 V=True,
4171 *R)
4173 if gmt.is_gmt5():
4174 gmt.grdimage(fn_grid, C=fn_cpt, E='i', n='l', *rxyj)
4176 else:
4177 gmt.grdimage(fn_grid, C=fn_cpt, E='i', S='l', *rxyj)
4179 if dpd.contour:
4180 gmt.grdcontour(fn_grid, C=fn_cpt, W='0.5p,black', *rxyj)
4181 innerticks = '0.5p,black'
4183 os.remove(fn_grid)
4184 os.remove(fn_mean)
4186 if dpd.method == 'fillcontour':
4187 extra = dict(C=fn_cpt)
4188 extra.update(dpd.extra)
4189 gmt.pscontour(in_columns=dpd.data,
4190 I=True, *rxyj, **extra) # noqa
4192 if dpd.method == 'contour':
4193 extra = dict(W='0.5p,black', C=fn_cpt)
4194 extra.update(dpd.extra)
4195 gmt.pscontour(in_columns=dpd.data, *rxyj, **extra)
4197 return fn_cpt, innerticks
4199 def draw_basemap(self, gmt, widget, scaler):
4200 gmt.psbasemap(*(widget.JXY() + scaler.RB(ax_projection=True)))
4202 def draw(self, gmt, widget, scaler):
4203 rxyj = scaler.R() + widget.JXY()
4204 for dat, sym in zip(self.data, self.symbols):
4205 gmt.psxy(in_columns=dat, *(sym.split()+rxyj))
4207 def post_draw(self, gmt, widget, scaler):
4208 pass
4210 def pre_draw(self, gmt, widget, scaler):
4211 pass
4213 def draw_extra(self, gmt, widget, scaler_x, scaler_y):
4215 for dat, sym in zip(self.data_x, self.symbols_x):
4216 gmt.psxy(in_columns=dat,
4217 *(sym.split() + scaler_x.R() + widget.JXY()))
4219 for dat, sym in zip(self.data_y, self.symbols_y):
4220 gmt.psxy(in_columns=dat,
4221 *(sym.split() + scaler_y.R() + widget.JXY()))
4223 def draw_text(self, gmt, widget, scaler):
4225 rxyj = scaler.R() + widget.JXY()
4226 for td in self.text_defs:
4227 x, y = td.data[0:2]
4228 text = td.data[-1]
4229 size = td.size
4230 angle = 0
4231 fontno = td.fontno
4232 justify = td.justify
4233 color = td.color
4234 if gmt.is_gmt5():
4235 gmt.pstext(
4236 in_rows=[(x, y, text)],
4237 F='+f%gp,%s,%s+a%g+j%s' % (
4238 size, fontno, color, angle, justify),
4239 D='%gp/%gp' % td.offset, *rxyj)
4240 else:
4241 gmt.pstext(
4242 in_rows=[(x, y, size, angle, fontno, justify, text)],
4243 D='%gp/%gp' % td.offset, *rxyj)
4245 def save(self, filename, resolution=150):
4247 conf = dict(self.default_config)
4248 conf.update(self.config)
4250 gmt, layout, widget, palette_widget = self.setup_base(conf)
4251 scaler = self.setup_scaling(conf)
4252 scaler_x, scaler_y = self.setup_scaling_extra(scaler, conf)
4254 self.setup_projection(widget, scaler, conf)
4255 if self.fixate_widget_aspect:
4256 aspect = aspect_for_projection(
4257 gmt.installation['version'], *(widget.J() + scaler.R()))
4259 widget.set_aspect(aspect)
4261 if conf['draw_layout']:
4262 gmt.draw_layout(layout)
4263 cptfile = None
4264 if self.density_plot_defs:
4265 cptfile, innerticks = self.draw_density(gmt, widget, scaler)
4266 self.pre_draw(gmt, widget, scaler)
4267 self.draw(gmt, widget, scaler)
4268 self.post_draw(gmt, widget, scaler)
4269 self.draw_extra(gmt, widget, scaler_x, scaler_y)
4270 self.draw_text(gmt, widget, scaler)
4271 self.draw_basemap(gmt, widget, scaler)
4273 if palette_widget and cptfile:
4274 nice_palette(gmt, palette_widget, scaler, cptfile,
4275 innerticks=innerticks,
4276 zlabeloffset=conf['zlabeloffset'])
4278 gmt.save(filename, resolution=resolution)
4281class LinLinPlot(Simple):
4282 pass
4285class LogLinPlot(Simple):
4287 def setup_defaults(self):
4288 self.set_defaults(xmode='min-max')
4290 def setup_projection(self, widget, scaler, conf):
4291 widget['J'] = '-JX%(width)gpl/%(height)gp'
4292 scaler['B'] = '-B2:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen'
4295class LinLogPlot(Simple):
4297 def setup_defaults(self):
4298 self.set_defaults(ymode='min-max')
4300 def setup_projection(self, widget, scaler, conf):
4301 widget['J'] = '-JX%(width)gp/%(height)gpl'
4302 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/2:%(ylabel)s:WSen'
4305class LogLogPlot(Simple):
4307 def setup_defaults(self):
4308 self.set_defaults(mode='min-max')
4310 def setup_projection(self, widget, scaler, conf):
4311 widget['J'] = '-JX%(width)gpl/%(height)gpl'
4312 scaler['B'] = '-B2:%(xlabel)s:/2:%(ylabel)s:WSen'
4315class AziDistPlot(Simple):
4317 def __init__(self, *args, **kwargs):
4318 Simple.__init__(self, *args, **kwargs)
4319 self.fixate_widget_aspect = True
4321 def setup_defaults(self):
4322 self.set_defaults(
4323 height=15.*cm,
4324 width=15.*cm,
4325 xmode='off',
4326 xlimits=(0., 360.),
4327 xinc=45.)
4329 def setup_projection(self, widget, scaler, conf):
4330 widget['J'] = '-JPa%(width)gp'
4332 def setup_scaling_plus(self, scaler, axes):
4333 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/%(yinc)g:%(ylabel)s:N'
4336class MPlot(Simple):
4338 def __init__(self, *args, **kwargs):
4339 Simple.__init__(self, *args, **kwargs)
4340 self.fixate_widget_aspect = True
4342 def setup_defaults(self):
4343 self.set_defaults(xmode='min-max', ymode='min-max')
4345 def setup_projection(self, widget, scaler, conf):
4346 par = scaler.get_params()
4347 lon0 = (par['xmin'] + par['xmax'])/2.
4348 lat0 = (par['ymin'] + par['ymax'])/2.
4349 sll = '%g/%g' % (lon0, lat0)
4350 widget['J'] = '-JM' + sll + '/%(width)gp'
4351 scaler['B'] = \
4352 '-B%(xinc)gg%(xinc)g:%(xlabel)s:/%(yinc)gg%(yinc)g:%(ylabel)s:WSen'
4355def nice_palette(gmt, widget, scaleguru, cptfile, zlabeloffset=0.8*inch,
4356 innerticks=True):
4358 par = scaleguru.get_params()
4359 par_ax = scaleguru.get_params(ax_projection=True)
4360 nz_palette = int(widget.height()/inch * 300)
4361 px = num.zeros(nz_palette*2)
4362 px[1::2] += 1
4363 pz = num.linspace(par['zmin'], par['zmax'], nz_palette).repeat(2)
4364 pdz = pz[2]-pz[0]
4365 palgrdfile = gmt.tempfilename()
4366 pal_r = (0, 1, par['zmin'], par['zmax'])
4367 pal_ax_r = (0, 1, par_ax['zmin'], par_ax['zmax'])
4368 gmt.xyz2grd(
4369 G=palgrdfile, R=pal_r,
4370 I=(1, pdz), in_columns=(px, pz, pz), # noqa
4371 out_discard=True)
4373 gmt.grdimage(palgrdfile, R=pal_r, C=cptfile, *widget.JXY())
4374 if isinstance(innerticks, str):
4375 tickpen = innerticks
4376 gmt.grdcontour(palgrdfile, W=tickpen, R=pal_r, C=cptfile,
4377 *widget.JXY())
4379 negpalwid = '%gp' % -widget.width()
4380 if not isinstance(innerticks, str) and innerticks:
4381 ticklen = negpalwid
4382 else:
4383 ticklen = '0p'
4385 TICK_LENGTH_PARAM = 'MAP_TICK_LENGTH' if gmt.is_gmt5() else 'TICK_LENGTH'
4386 gmt.psbasemap(
4387 R=pal_ax_r, B='4::/%(zinc)g::nsw' % par_ax,
4388 config={TICK_LENGTH_PARAM: ticklen},
4389 *widget.JXY())
4391 if innerticks:
4392 gmt.psbasemap(
4393 R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax,
4394 config={TICK_LENGTH_PARAM: '0p'},
4395 *widget.JXY())
4396 else:
4397 gmt.psbasemap(R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax, *widget.JXY())
4399 if par_ax['zlabel']:
4400 label_font = gmt.label_font()
4401 label_font_size = gmt.label_font_size()
4402 label_offset = zlabeloffset
4403 gmt.pstext(
4404 R=(0, 1, 0, 2), D="%gp/0p" % label_offset,
4405 N=True,
4406 in_rows=[(1, 1, label_font_size, -90, label_font, 'CB',
4407 par_ax['zlabel'])],
4408 *widget.JXY())