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.
12import subprocess
13try:
14 from StringIO import StringIO as BytesIO
15except ImportError:
16 from io import BytesIO
17import re
18import os
19import sys
20import shutil
21from os.path import join as pjoin
22import tempfile
23import random
24import logging
25import math
26import numpy as num
27import copy
28from select import select
29try:
30 from scipy.io import netcdf_file
31except ImportError:
32 from scipy.io.netcdf import netcdf_file
34from pyrocko import ExternalProgramMissing
37find_bb = re.compile(br'%%BoundingBox:((\s+[-0-9]+){4})')
38find_hiresbb = re.compile(br'%%HiResBoundingBox:((\s+[-0-9.]+){4})')
41encoding_gmt_to_python = {
42 'isolatin1+': 'iso-8859-1',
43 'standard+': 'ascii',
44 'isolatin1': 'iso-8859-1',
45 'standard': 'ascii'}
47for i in range(1, 11):
48 encoding_gmt_to_python['iso-8859-%i' % i] = 'iso-8859-%i' % i
51def have_gmt():
52 try:
53 get_gmt_installation('newest')
54 return True
56 except GMTInstallationProblem:
57 return False
60def check_have_gmt():
61 if not have_gmt():
62 raise ExternalProgramMissing('GMT is not installed or cannot be found')
65def have_pixmaptools():
66 for prog in [['pdftocairo'], ['convert'], ['gs', '-h']]:
67 try:
68 p = subprocess.Popen(
69 prog,
70 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
72 (stdout, stderr) = p.communicate()
74 except OSError:
75 return False
77 return True
80class GmtPyError(Exception):
81 pass
84class GMTError(GmtPyError):
85 pass
88class GMTInstallationProblem(GmtPyError):
89 pass
92def convert_graph(in_filename, out_filename, resolution=75., oversample=2.,
93 width=None, height=None, size=None):
95 _, tmp_filename_base = tempfile.mkstemp()
97 try:
98 if out_filename.endswith('.svg'):
99 fmt_arg = '-svg'
100 tmp_filename = tmp_filename_base
101 oversample = 1.0
102 else:
103 fmt_arg = '-png'
104 tmp_filename = tmp_filename_base + '-1.png'
106 if size is not None:
107 scale_args = ['-scale-to', '%i' % int(round(size*oversample))]
108 elif width is not None:
109 scale_args = ['-scale-to-x', '%i' % int(round(width*oversample))]
110 elif height is not None:
111 scale_args = ['-scale-to-y', '%i' % int(round(height*oversample))]
112 else:
113 scale_args = ['-r', '%i' % int(round(resolution * oversample))]
115 try:
116 subprocess.check_call(
117 ['pdftocairo'] + scale_args +
118 [fmt_arg, in_filename, tmp_filename_base])
119 except OSError as e:
120 raise GmtPyError(
121 'Cannot start `pdftocairo`, is it installed? (%s)' % str(e))
123 if oversample > 1.:
124 try:
125 subprocess.check_call([
126 'convert',
127 tmp_filename,
128 '-resize', '%i%%' % int(round(100.0/oversample)),
129 out_filename])
130 except OSError as e:
131 raise GmtPyError(
132 'Cannot start `convert`, is it installed? (%s)' % str(e))
134 else:
135 if out_filename.endswith('.png') or out_filename.endswith('.svg'):
136 shutil.move(tmp_filename, out_filename)
137 else:
138 try:
139 subprocess.check_call(
140 ['convert', tmp_filename, out_filename])
141 except Exception as e:
142 raise GmtPyError(
143 'Cannot start `convert`, is it installed? (%s)'
144 % str(e))
146 except Exception:
147 raise
149 finally:
150 if os.path.exists(tmp_filename_base):
151 os.remove(tmp_filename_base)
153 if os.path.exists(tmp_filename):
154 os.remove(tmp_filename)
157def get_bbox(s):
158 for pat in [find_hiresbb, find_bb]:
159 m = pat.search(s)
160 if m:
161 bb = [float(x) for x in m.group(1).split()]
162 return bb
164 raise GmtPyError('Cannot find bbox')
167def replace_bbox(bbox, *args):
169 def repl(m):
170 if m.group(1):
171 return ('%%HiResBoundingBox: ' + ' '.join(
172 '%.3f' % float(x) for x in bbox)).encode('ascii')
173 else:
174 return ('%%%%BoundingBox: %i %i %i %i' % (
175 int(math.floor(bbox[0])),
176 int(math.floor(bbox[1])),
177 int(math.ceil(bbox[2])),
178 int(math.ceil(bbox[3])))).encode('ascii')
180 pat = re.compile(br'%%(HiRes)?BoundingBox:((\s+[0-9.]+){4})')
181 if len(args) == 1:
182 s = args[0]
183 return pat.sub(repl, s)
185 else:
186 fin, fout = args
187 nn = 0
188 for line in fin:
189 line, n = pat.subn(repl, line)
190 nn += n
191 fout.write(line)
192 if nn == 2:
193 break
195 if nn == 2:
196 for line in fin:
197 fout.write(line)
200def escape_shell_arg(s):
201 '''
202 This function should be used for debugging output only - it could be
203 insecure.
204 '''
206 if re.search(r'[^a-zA-Z0-9._/=-]', s):
207 return "'" + s.replace("'", "'\\''") + "'"
208 else:
209 return s
212def escape_shell_args(args):
213 '''
214 This function should be used for debugging output only - it could be
215 insecure.
216 '''
218 return ' '.join([escape_shell_arg(x) for x in args])
221golden_ratio = 1.61803
223# units in points
224_units = {
225 'i': 72.,
226 'c': 72./2.54,
227 'm': 72.*100./2.54,
228 'p': 1.}
230inch = _units['i']
231cm = _units['c']
233# some awsome colors
234tango_colors = {
235 'butter1': (252, 233, 79),
236 'butter2': (237, 212, 0),
237 'butter3': (196, 160, 0),
238 'chameleon1': (138, 226, 52),
239 'chameleon2': (115, 210, 22),
240 'chameleon3': (78, 154, 6),
241 'orange1': (252, 175, 62),
242 'orange2': (245, 121, 0),
243 'orange3': (206, 92, 0),
244 'skyblue1': (114, 159, 207),
245 'skyblue2': (52, 101, 164),
246 'skyblue3': (32, 74, 135),
247 'plum1': (173, 127, 168),
248 'plum2': (117, 80, 123),
249 'plum3': (92, 53, 102),
250 'chocolate1': (233, 185, 110),
251 'chocolate2': (193, 125, 17),
252 'chocolate3': (143, 89, 2),
253 'scarletred1': (239, 41, 41),
254 'scarletred2': (204, 0, 0),
255 'scarletred3': (164, 0, 0),
256 'aluminium1': (238, 238, 236),
257 'aluminium2': (211, 215, 207),
258 'aluminium3': (186, 189, 182),
259 'aluminium4': (136, 138, 133),
260 'aluminium5': (85, 87, 83),
261 'aluminium6': (46, 52, 54)
262}
264graph_colors = [tango_colors[_x] for _x in (
265 'scarletred2', 'skyblue3', 'chameleon3', 'orange2', 'plum2', 'chocolate2',
266 'butter2')]
269def color(x=None):
270 '''
271 Generate a string for GMT option arguments expecting a color.
273 If ``x`` is None, a random color is returned. If it is an integer, the
274 corresponding ``gmtpy.graph_colors[x]`` or black returned. If it is a
275 string and the corresponding ``gmtpy.tango_colors[x]`` exists, this is
276 returned, or the string is passed through. If ``x`` is a tuple, it is
277 transformed into the string form which GMT expects.
278 '''
280 if x is None:
281 return '%i/%i/%i' % tuple(random.randint(0, 255) for _ in 'rgb')
283 if isinstance(x, int):
284 if 0 <= x < len(graph_colors):
285 return '%i/%i/%i' % graph_colors[x]
286 else:
287 return '0/0/0'
289 elif isinstance(x, str):
290 if x in tango_colors:
291 return '%i/%i/%i' % tango_colors[x]
292 else:
293 return x
295 return '%i/%i/%i' % x
298def color_tup(x=None):
299 if x is None:
300 return tuple([random.randint(0, 255) for _x in 'rgb'])
302 if isinstance(x, int):
303 if 0 <= x < len(graph_colors):
304 return graph_colors[x]
305 else:
306 return (0, 0, 0)
308 elif isinstance(x, str):
309 if x in tango_colors:
310 return tango_colors[x]
312 return x
315_gmt_installations = {}
317# Set fixed installation(s) to use...
318# (use this, if you want to use different GMT versions simultaneously.)
320# _gmt_installations['4.2.1'] = {'home': '/sw/etch-ia32/gmt-4.2.1',
321# 'bin': '/sw/etch-ia32/gmt-4.2.1/bin'}
322# _gmt_installations['4.3.0'] = {'home': '/sw/etch-ia32/gmt-4.3.0',
323# 'bin': '/sw/etch-ia32/gmt-4.3.0/bin'}
324# _gmt_installations['6.0.0'] = {'home': '/usr/share/gmt',
325# 'bin': '/usr/bin' }
327# ... or let GmtPy autodetect GMT via $PATH and $GMTHOME
330def key_version(a):
331 a = a.split('_')[0] # get rid of revision id
332 return [int(x) for x in a.split('.')]
335def newest_installed_gmt_version():
336 return sorted(_gmt_installations.keys(), key=key_version)[-1]
339def all_installed_gmt_versions():
340 return sorted(_gmt_installations.keys(), key=key_version)
343# To have consistent defaults, they are hardcoded here and should not be
344# changed.
346_gmt_defaults_by_version = {}
347_gmt_defaults_by_version['4.2.1'] = r'''
348#
349# GMT-SYSTEM 4.2.1 Defaults file
350#
351#-------- Plot Media Parameters -------------
352PAGE_COLOR = 255/255/255
353PAGE_ORIENTATION = portrait
354PAPER_MEDIA = a4+
355#-------- Basemap Annotation Parameters ------
356ANNOT_MIN_ANGLE = 20
357ANNOT_MIN_SPACING = 0
358ANNOT_FONT_PRIMARY = Helvetica
359ANNOT_FONT_SIZE = 12p
360ANNOT_OFFSET_PRIMARY = 0.075i
361ANNOT_FONT_SECONDARY = Helvetica
362ANNOT_FONT_SIZE_SECONDARY = 16p
363ANNOT_OFFSET_SECONDARY = 0.075i
364DEGREE_SYMBOL = ring
365HEADER_FONT = Helvetica
366HEADER_FONT_SIZE = 36p
367HEADER_OFFSET = 0.1875i
368LABEL_FONT = Helvetica
369LABEL_FONT_SIZE = 14p
370LABEL_OFFSET = 0.1125i
371OBLIQUE_ANNOTATION = 1
372PLOT_CLOCK_FORMAT = hh:mm:ss
373PLOT_DATE_FORMAT = yyyy-mm-dd
374PLOT_DEGREE_FORMAT = +ddd:mm:ss
375Y_AXIS_TYPE = hor_text
376#-------- Basemap Layout Parameters ---------
377BASEMAP_AXES = WESN
378BASEMAP_FRAME_RGB = 0/0/0
379BASEMAP_TYPE = plain
380FRAME_PEN = 1.25p
381FRAME_WIDTH = 0.075i
382GRID_CROSS_SIZE_PRIMARY = 0i
383GRID_CROSS_SIZE_SECONDARY = 0i
384GRID_PEN_PRIMARY = 0.25p
385GRID_PEN_SECONDARY = 0.5p
386MAP_SCALE_HEIGHT = 0.075i
387TICK_LENGTH = 0.075i
388POLAR_CAP = 85/90
389TICK_PEN = 0.5p
390X_AXIS_LENGTH = 9i
391Y_AXIS_LENGTH = 6i
392X_ORIGIN = 1i
393Y_ORIGIN = 1i
394UNIX_TIME = FALSE
395UNIX_TIME_POS = -0.75i/-0.75i
396#-------- Color System Parameters -----------
397COLOR_BACKGROUND = 0/0/0
398COLOR_FOREGROUND = 255/255/255
399COLOR_NAN = 128/128/128
400COLOR_IMAGE = adobe
401COLOR_MODEL = rgb
402HSV_MIN_SATURATION = 1
403HSV_MAX_SATURATION = 0.1
404HSV_MIN_VALUE = 0.3
405HSV_MAX_VALUE = 1
406#-------- PostScript Parameters -------------
407CHAR_ENCODING = ISOLatin1+
408DOTS_PR_INCH = 300
409N_COPIES = 1
410PS_COLOR = rgb
411PS_IMAGE_COMPRESS = none
412PS_IMAGE_FORMAT = ascii
413PS_LINE_CAP = round
414PS_LINE_JOIN = miter
415PS_MITER_LIMIT = 35
416PS_VERBOSE = FALSE
417GLOBAL_X_SCALE = 1
418GLOBAL_Y_SCALE = 1
419#-------- I/O Format Parameters -------------
420D_FORMAT = %lg
421FIELD_DELIMITER = tab
422GRIDFILE_SHORTHAND = FALSE
423GRID_FORMAT = nf
424INPUT_CLOCK_FORMAT = hh:mm:ss
425INPUT_DATE_FORMAT = yyyy-mm-dd
426IO_HEADER = FALSE
427N_HEADER_RECS = 1
428OUTPUT_CLOCK_FORMAT = hh:mm:ss
429OUTPUT_DATE_FORMAT = yyyy-mm-dd
430OUTPUT_DEGREE_FORMAT = +D
431XY_TOGGLE = FALSE
432#-------- Projection Parameters -------------
433ELLIPSOID = WGS-84
434MAP_SCALE_FACTOR = default
435MEASURE_UNIT = inch
436#-------- Calendar/Time Parameters ----------
437TIME_FORMAT_PRIMARY = full
438TIME_FORMAT_SECONDARY = full
439TIME_EPOCH = 2000-01-01T00:00:00
440TIME_IS_INTERVAL = OFF
441TIME_INTERVAL_FRACTION = 0.5
442TIME_LANGUAGE = us
443TIME_SYSTEM = other
444TIME_UNIT = d
445TIME_WEEK_START = Sunday
446Y2K_OFFSET_YEAR = 1950
447#-------- Miscellaneous Parameters ----------
448HISTORY = TRUE
449INTERPOLANT = akima
450LINE_STEP = 0.01i
451VECTOR_SHAPE = 0
452VERBOSE = FALSE'''
454_gmt_defaults_by_version['4.3.0'] = r'''
455#
456# GMT-SYSTEM 4.3.0 Defaults file
457#
458#-------- Plot Media Parameters -------------
459PAGE_COLOR = 255/255/255
460PAGE_ORIENTATION = portrait
461PAPER_MEDIA = a4+
462#-------- Basemap Annotation Parameters ------
463ANNOT_MIN_ANGLE = 20
464ANNOT_MIN_SPACING = 0
465ANNOT_FONT_PRIMARY = Helvetica
466ANNOT_FONT_SIZE_PRIMARY = 12p
467ANNOT_OFFSET_PRIMARY = 0.075i
468ANNOT_FONT_SECONDARY = Helvetica
469ANNOT_FONT_SIZE_SECONDARY = 16p
470ANNOT_OFFSET_SECONDARY = 0.075i
471DEGREE_SYMBOL = ring
472HEADER_FONT = Helvetica
473HEADER_FONT_SIZE = 36p
474HEADER_OFFSET = 0.1875i
475LABEL_FONT = Helvetica
476LABEL_FONT_SIZE = 14p
477LABEL_OFFSET = 0.1125i
478OBLIQUE_ANNOTATION = 1
479PLOT_CLOCK_FORMAT = hh:mm:ss
480PLOT_DATE_FORMAT = yyyy-mm-dd
481PLOT_DEGREE_FORMAT = +ddd:mm:ss
482Y_AXIS_TYPE = hor_text
483#-------- Basemap Layout Parameters ---------
484BASEMAP_AXES = WESN
485BASEMAP_FRAME_RGB = 0/0/0
486BASEMAP_TYPE = plain
487FRAME_PEN = 1.25p
488FRAME_WIDTH = 0.075i
489GRID_CROSS_SIZE_PRIMARY = 0i
490GRID_PEN_PRIMARY = 0.25p
491GRID_CROSS_SIZE_SECONDARY = 0i
492GRID_PEN_SECONDARY = 0.5p
493MAP_SCALE_HEIGHT = 0.075i
494POLAR_CAP = 85/90
495TICK_LENGTH = 0.075i
496TICK_PEN = 0.5p
497X_AXIS_LENGTH = 9i
498Y_AXIS_LENGTH = 6i
499X_ORIGIN = 1i
500Y_ORIGIN = 1i
501UNIX_TIME = FALSE
502UNIX_TIME_POS = BL/-0.75i/-0.75i
503UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
504#-------- Color System Parameters -----------
505COLOR_BACKGROUND = 0/0/0
506COLOR_FOREGROUND = 255/255/255
507COLOR_NAN = 128/128/128
508COLOR_IMAGE = adobe
509COLOR_MODEL = rgb
510HSV_MIN_SATURATION = 1
511HSV_MAX_SATURATION = 0.1
512HSV_MIN_VALUE = 0.3
513HSV_MAX_VALUE = 1
514#-------- PostScript Parameters -------------
515CHAR_ENCODING = ISOLatin1+
516DOTS_PR_INCH = 300
517N_COPIES = 1
518PS_COLOR = rgb
519PS_IMAGE_COMPRESS = none
520PS_IMAGE_FORMAT = ascii
521PS_LINE_CAP = round
522PS_LINE_JOIN = miter
523PS_MITER_LIMIT = 35
524PS_VERBOSE = FALSE
525GLOBAL_X_SCALE = 1
526GLOBAL_Y_SCALE = 1
527#-------- I/O Format Parameters -------------
528D_FORMAT = %lg
529FIELD_DELIMITER = tab
530GRIDFILE_SHORTHAND = FALSE
531GRID_FORMAT = nf
532INPUT_CLOCK_FORMAT = hh:mm:ss
533INPUT_DATE_FORMAT = yyyy-mm-dd
534IO_HEADER = FALSE
535N_HEADER_RECS = 1
536OUTPUT_CLOCK_FORMAT = hh:mm:ss
537OUTPUT_DATE_FORMAT = yyyy-mm-dd
538OUTPUT_DEGREE_FORMAT = +D
539XY_TOGGLE = FALSE
540#-------- Projection Parameters -------------
541ELLIPSOID = WGS-84
542MAP_SCALE_FACTOR = default
543MEASURE_UNIT = inch
544#-------- Calendar/Time Parameters ----------
545TIME_FORMAT_PRIMARY = full
546TIME_FORMAT_SECONDARY = full
547TIME_EPOCH = 2000-01-01T00:00:00
548TIME_IS_INTERVAL = OFF
549TIME_INTERVAL_FRACTION = 0.5
550TIME_LANGUAGE = us
551TIME_UNIT = d
552TIME_WEEK_START = Sunday
553Y2K_OFFSET_YEAR = 1950
554#-------- Miscellaneous Parameters ----------
555HISTORY = TRUE
556INTERPOLANT = akima
557LINE_STEP = 0.01i
558VECTOR_SHAPE = 0
559VERBOSE = FALSE'''
562_gmt_defaults_by_version['4.3.1'] = r'''
563#
564# GMT-SYSTEM 4.3.1 Defaults file
565#
566#-------- Plot Media Parameters -------------
567PAGE_COLOR = 255/255/255
568PAGE_ORIENTATION = portrait
569PAPER_MEDIA = a4+
570#-------- Basemap Annotation Parameters ------
571ANNOT_MIN_ANGLE = 20
572ANNOT_MIN_SPACING = 0
573ANNOT_FONT_PRIMARY = Helvetica
574ANNOT_FONT_SIZE_PRIMARY = 12p
575ANNOT_OFFSET_PRIMARY = 0.075i
576ANNOT_FONT_SECONDARY = Helvetica
577ANNOT_FONT_SIZE_SECONDARY = 16p
578ANNOT_OFFSET_SECONDARY = 0.075i
579DEGREE_SYMBOL = ring
580HEADER_FONT = Helvetica
581HEADER_FONT_SIZE = 36p
582HEADER_OFFSET = 0.1875i
583LABEL_FONT = Helvetica
584LABEL_FONT_SIZE = 14p
585LABEL_OFFSET = 0.1125i
586OBLIQUE_ANNOTATION = 1
587PLOT_CLOCK_FORMAT = hh:mm:ss
588PLOT_DATE_FORMAT = yyyy-mm-dd
589PLOT_DEGREE_FORMAT = +ddd:mm:ss
590Y_AXIS_TYPE = hor_text
591#-------- Basemap Layout Parameters ---------
592BASEMAP_AXES = WESN
593BASEMAP_FRAME_RGB = 0/0/0
594BASEMAP_TYPE = plain
595FRAME_PEN = 1.25p
596FRAME_WIDTH = 0.075i
597GRID_CROSS_SIZE_PRIMARY = 0i
598GRID_PEN_PRIMARY = 0.25p
599GRID_CROSS_SIZE_SECONDARY = 0i
600GRID_PEN_SECONDARY = 0.5p
601MAP_SCALE_HEIGHT = 0.075i
602POLAR_CAP = 85/90
603TICK_LENGTH = 0.075i
604TICK_PEN = 0.5p
605X_AXIS_LENGTH = 9i
606Y_AXIS_LENGTH = 6i
607X_ORIGIN = 1i
608Y_ORIGIN = 1i
609UNIX_TIME = FALSE
610UNIX_TIME_POS = BL/-0.75i/-0.75i
611UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
612#-------- Color System Parameters -----------
613COLOR_BACKGROUND = 0/0/0
614COLOR_FOREGROUND = 255/255/255
615COLOR_NAN = 128/128/128
616COLOR_IMAGE = adobe
617COLOR_MODEL = rgb
618HSV_MIN_SATURATION = 1
619HSV_MAX_SATURATION = 0.1
620HSV_MIN_VALUE = 0.3
621HSV_MAX_VALUE = 1
622#-------- PostScript Parameters -------------
623CHAR_ENCODING = ISOLatin1+
624DOTS_PR_INCH = 300
625N_COPIES = 1
626PS_COLOR = rgb
627PS_IMAGE_COMPRESS = none
628PS_IMAGE_FORMAT = ascii
629PS_LINE_CAP = round
630PS_LINE_JOIN = miter
631PS_MITER_LIMIT = 35
632PS_VERBOSE = FALSE
633GLOBAL_X_SCALE = 1
634GLOBAL_Y_SCALE = 1
635#-------- I/O Format Parameters -------------
636D_FORMAT = %lg
637FIELD_DELIMITER = tab
638GRIDFILE_SHORTHAND = FALSE
639GRID_FORMAT = nf
640INPUT_CLOCK_FORMAT = hh:mm:ss
641INPUT_DATE_FORMAT = yyyy-mm-dd
642IO_HEADER = FALSE
643N_HEADER_RECS = 1
644OUTPUT_CLOCK_FORMAT = hh:mm:ss
645OUTPUT_DATE_FORMAT = yyyy-mm-dd
646OUTPUT_DEGREE_FORMAT = +D
647XY_TOGGLE = FALSE
648#-------- Projection Parameters -------------
649ELLIPSOID = WGS-84
650MAP_SCALE_FACTOR = default
651MEASURE_UNIT = inch
652#-------- Calendar/Time Parameters ----------
653TIME_FORMAT_PRIMARY = full
654TIME_FORMAT_SECONDARY = full
655TIME_EPOCH = 2000-01-01T00:00:00
656TIME_IS_INTERVAL = OFF
657TIME_INTERVAL_FRACTION = 0.5
658TIME_LANGUAGE = us
659TIME_UNIT = d
660TIME_WEEK_START = Sunday
661Y2K_OFFSET_YEAR = 1950
662#-------- Miscellaneous Parameters ----------
663HISTORY = TRUE
664INTERPOLANT = akima
665LINE_STEP = 0.01i
666VECTOR_SHAPE = 0
667VERBOSE = FALSE'''
670_gmt_defaults_by_version['4.4.0'] = r'''
671#
672# GMT-SYSTEM 4.4.0 [64-bit] Defaults file
673#
674#-------- Plot Media Parameters -------------
675PAGE_COLOR = 255/255/255
676PAGE_ORIENTATION = portrait
677PAPER_MEDIA = a4+
678#-------- Basemap Annotation Parameters ------
679ANNOT_MIN_ANGLE = 20
680ANNOT_MIN_SPACING = 0
681ANNOT_FONT_PRIMARY = Helvetica
682ANNOT_FONT_SIZE_PRIMARY = 14p
683ANNOT_OFFSET_PRIMARY = 0.075i
684ANNOT_FONT_SECONDARY = Helvetica
685ANNOT_FONT_SIZE_SECONDARY = 16p
686ANNOT_OFFSET_SECONDARY = 0.075i
687DEGREE_SYMBOL = ring
688HEADER_FONT = Helvetica
689HEADER_FONT_SIZE = 36p
690HEADER_OFFSET = 0.1875i
691LABEL_FONT = Helvetica
692LABEL_FONT_SIZE = 14p
693LABEL_OFFSET = 0.1125i
694OBLIQUE_ANNOTATION = 1
695PLOT_CLOCK_FORMAT = hh:mm:ss
696PLOT_DATE_FORMAT = yyyy-mm-dd
697PLOT_DEGREE_FORMAT = +ddd:mm:ss
698Y_AXIS_TYPE = hor_text
699#-------- Basemap Layout Parameters ---------
700BASEMAP_AXES = WESN
701BASEMAP_FRAME_RGB = 0/0/0
702BASEMAP_TYPE = plain
703FRAME_PEN = 1.25p
704FRAME_WIDTH = 0.075i
705GRID_CROSS_SIZE_PRIMARY = 0i
706GRID_PEN_PRIMARY = 0.25p
707GRID_CROSS_SIZE_SECONDARY = 0i
708GRID_PEN_SECONDARY = 0.5p
709MAP_SCALE_HEIGHT = 0.075i
710POLAR_CAP = 85/90
711TICK_LENGTH = 0.075i
712TICK_PEN = 0.5p
713X_AXIS_LENGTH = 9i
714Y_AXIS_LENGTH = 6i
715X_ORIGIN = 1i
716Y_ORIGIN = 1i
717UNIX_TIME = FALSE
718UNIX_TIME_POS = BL/-0.75i/-0.75i
719UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
720#-------- Color System Parameters -----------
721COLOR_BACKGROUND = 0/0/0
722COLOR_FOREGROUND = 255/255/255
723COLOR_NAN = 128/128/128
724COLOR_IMAGE = adobe
725COLOR_MODEL = rgb
726HSV_MIN_SATURATION = 1
727HSV_MAX_SATURATION = 0.1
728HSV_MIN_VALUE = 0.3
729HSV_MAX_VALUE = 1
730#-------- PostScript Parameters -------------
731CHAR_ENCODING = ISOLatin1+
732DOTS_PR_INCH = 300
733N_COPIES = 1
734PS_COLOR = rgb
735PS_IMAGE_COMPRESS = lzw
736PS_IMAGE_FORMAT = ascii
737PS_LINE_CAP = round
738PS_LINE_JOIN = miter
739PS_MITER_LIMIT = 35
740PS_VERBOSE = FALSE
741GLOBAL_X_SCALE = 1
742GLOBAL_Y_SCALE = 1
743#-------- I/O Format Parameters -------------
744D_FORMAT = %lg
745FIELD_DELIMITER = tab
746GRIDFILE_SHORTHAND = FALSE
747GRID_FORMAT = nf
748INPUT_CLOCK_FORMAT = hh:mm:ss
749INPUT_DATE_FORMAT = yyyy-mm-dd
750IO_HEADER = FALSE
751N_HEADER_RECS = 1
752OUTPUT_CLOCK_FORMAT = hh:mm:ss
753OUTPUT_DATE_FORMAT = yyyy-mm-dd
754OUTPUT_DEGREE_FORMAT = +D
755XY_TOGGLE = FALSE
756#-------- Projection Parameters -------------
757ELLIPSOID = WGS-84
758MAP_SCALE_FACTOR = default
759MEASURE_UNIT = inch
760#-------- Calendar/Time Parameters ----------
761TIME_FORMAT_PRIMARY = full
762TIME_FORMAT_SECONDARY = full
763TIME_EPOCH = 2000-01-01T00:00:00
764TIME_IS_INTERVAL = OFF
765TIME_INTERVAL_FRACTION = 0.5
766TIME_LANGUAGE = us
767TIME_UNIT = d
768TIME_WEEK_START = Sunday
769Y2K_OFFSET_YEAR = 1950
770#-------- Miscellaneous Parameters ----------
771HISTORY = TRUE
772INTERPOLANT = akima
773LINE_STEP = 0.01i
774VECTOR_SHAPE = 0
775VERBOSE = FALSE
776'''
778_gmt_defaults_by_version['4.5.2'] = r'''
779#
780# GMT-SYSTEM 4.5.2 [64-bit] Defaults file
781#
782#-------- Plot Media Parameters -------------
783PAGE_COLOR = white
784PAGE_ORIENTATION = portrait
785PAPER_MEDIA = a4+
786#-------- Basemap Annotation Parameters ------
787ANNOT_MIN_ANGLE = 20
788ANNOT_MIN_SPACING = 0
789ANNOT_FONT_PRIMARY = Helvetica
790ANNOT_FONT_SIZE_PRIMARY = 14p
791ANNOT_OFFSET_PRIMARY = 0.075i
792ANNOT_FONT_SECONDARY = Helvetica
793ANNOT_FONT_SIZE_SECONDARY = 16p
794ANNOT_OFFSET_SECONDARY = 0.075i
795DEGREE_SYMBOL = ring
796HEADER_FONT = Helvetica
797HEADER_FONT_SIZE = 36p
798HEADER_OFFSET = 0.1875i
799LABEL_FONT = Helvetica
800LABEL_FONT_SIZE = 14p
801LABEL_OFFSET = 0.1125i
802OBLIQUE_ANNOTATION = 1
803PLOT_CLOCK_FORMAT = hh:mm:ss
804PLOT_DATE_FORMAT = yyyy-mm-dd
805PLOT_DEGREE_FORMAT = +ddd:mm:ss
806Y_AXIS_TYPE = hor_text
807#-------- Basemap Layout Parameters ---------
808BASEMAP_AXES = WESN
809BASEMAP_FRAME_RGB = black
810BASEMAP_TYPE = plain
811FRAME_PEN = 1.25p
812FRAME_WIDTH = 0.075i
813GRID_CROSS_SIZE_PRIMARY = 0i
814GRID_PEN_PRIMARY = 0.25p
815GRID_CROSS_SIZE_SECONDARY = 0i
816GRID_PEN_SECONDARY = 0.5p
817MAP_SCALE_HEIGHT = 0.075i
818POLAR_CAP = 85/90
819TICK_LENGTH = 0.075i
820TICK_PEN = 0.5p
821X_AXIS_LENGTH = 9i
822Y_AXIS_LENGTH = 6i
823X_ORIGIN = 1i
824Y_ORIGIN = 1i
825UNIX_TIME = FALSE
826UNIX_TIME_POS = BL/-0.75i/-0.75i
827UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
828#-------- Color System Parameters -----------
829COLOR_BACKGROUND = black
830COLOR_FOREGROUND = white
831COLOR_NAN = 128
832COLOR_IMAGE = adobe
833COLOR_MODEL = rgb
834HSV_MIN_SATURATION = 1
835HSV_MAX_SATURATION = 0.1
836HSV_MIN_VALUE = 0.3
837HSV_MAX_VALUE = 1
838#-------- PostScript Parameters -------------
839CHAR_ENCODING = ISOLatin1+
840DOTS_PR_INCH = 300
841GLOBAL_X_SCALE = 1
842GLOBAL_Y_SCALE = 1
843N_COPIES = 1
844PS_COLOR = rgb
845PS_IMAGE_COMPRESS = lzw
846PS_IMAGE_FORMAT = ascii
847PS_LINE_CAP = round
848PS_LINE_JOIN = miter
849PS_MITER_LIMIT = 35
850PS_VERBOSE = FALSE
851TRANSPARENCY = 0
852#-------- I/O Format Parameters -------------
853D_FORMAT = %.12lg
854FIELD_DELIMITER = tab
855GRIDFILE_FORMAT = nf
856GRIDFILE_SHORTHAND = FALSE
857INPUT_CLOCK_FORMAT = hh:mm:ss
858INPUT_DATE_FORMAT = yyyy-mm-dd
859IO_HEADER = FALSE
860N_HEADER_RECS = 1
861NAN_RECORDS = pass
862OUTPUT_CLOCK_FORMAT = hh:mm:ss
863OUTPUT_DATE_FORMAT = yyyy-mm-dd
864OUTPUT_DEGREE_FORMAT = D
865XY_TOGGLE = FALSE
866#-------- Projection Parameters -------------
867ELLIPSOID = WGS-84
868MAP_SCALE_FACTOR = default
869MEASURE_UNIT = inch
870#-------- Calendar/Time Parameters ----------
871TIME_FORMAT_PRIMARY = full
872TIME_FORMAT_SECONDARY = full
873TIME_EPOCH = 2000-01-01T00:00:00
874TIME_IS_INTERVAL = OFF
875TIME_INTERVAL_FRACTION = 0.5
876TIME_LANGUAGE = us
877TIME_UNIT = d
878TIME_WEEK_START = Sunday
879Y2K_OFFSET_YEAR = 1950
880#-------- Miscellaneous Parameters ----------
881HISTORY = TRUE
882INTERPOLANT = akima
883LINE_STEP = 0.01i
884VECTOR_SHAPE = 0
885VERBOSE = FALSE
886'''
888_gmt_defaults_by_version['4.5.3'] = r'''
889#
890# GMT-SYSTEM 4.5.3 (CVS Jun 18 2010 10:56:07) [64-bit] Defaults file
891#
892#-------- Plot Media Parameters -------------
893PAGE_COLOR = white
894PAGE_ORIENTATION = portrait
895PAPER_MEDIA = a4+
896#-------- Basemap Annotation Parameters ------
897ANNOT_MIN_ANGLE = 20
898ANNOT_MIN_SPACING = 0
899ANNOT_FONT_PRIMARY = Helvetica
900ANNOT_FONT_SIZE_PRIMARY = 14p
901ANNOT_OFFSET_PRIMARY = 0.075i
902ANNOT_FONT_SECONDARY = Helvetica
903ANNOT_FONT_SIZE_SECONDARY = 16p
904ANNOT_OFFSET_SECONDARY = 0.075i
905DEGREE_SYMBOL = ring
906HEADER_FONT = Helvetica
907HEADER_FONT_SIZE = 36p
908HEADER_OFFSET = 0.1875i
909LABEL_FONT = Helvetica
910LABEL_FONT_SIZE = 14p
911LABEL_OFFSET = 0.1125i
912OBLIQUE_ANNOTATION = 1
913PLOT_CLOCK_FORMAT = hh:mm:ss
914PLOT_DATE_FORMAT = yyyy-mm-dd
915PLOT_DEGREE_FORMAT = +ddd:mm:ss
916Y_AXIS_TYPE = hor_text
917#-------- Basemap Layout Parameters ---------
918BASEMAP_AXES = WESN
919BASEMAP_FRAME_RGB = black
920BASEMAP_TYPE = plain
921FRAME_PEN = 1.25p
922FRAME_WIDTH = 0.075i
923GRID_CROSS_SIZE_PRIMARY = 0i
924GRID_PEN_PRIMARY = 0.25p
925GRID_CROSS_SIZE_SECONDARY = 0i
926GRID_PEN_SECONDARY = 0.5p
927MAP_SCALE_HEIGHT = 0.075i
928POLAR_CAP = 85/90
929TICK_LENGTH = 0.075i
930TICK_PEN = 0.5p
931X_AXIS_LENGTH = 9i
932Y_AXIS_LENGTH = 6i
933X_ORIGIN = 1i
934Y_ORIGIN = 1i
935UNIX_TIME = FALSE
936UNIX_TIME_POS = BL/-0.75i/-0.75i
937UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S
938#-------- Color System Parameters -----------
939COLOR_BACKGROUND = black
940COLOR_FOREGROUND = white
941COLOR_NAN = 128
942COLOR_IMAGE = adobe
943COLOR_MODEL = rgb
944HSV_MIN_SATURATION = 1
945HSV_MAX_SATURATION = 0.1
946HSV_MIN_VALUE = 0.3
947HSV_MAX_VALUE = 1
948#-------- PostScript Parameters -------------
949CHAR_ENCODING = ISOLatin1+
950DOTS_PR_INCH = 300
951GLOBAL_X_SCALE = 1
952GLOBAL_Y_SCALE = 1
953N_COPIES = 1
954PS_COLOR = rgb
955PS_IMAGE_COMPRESS = lzw
956PS_IMAGE_FORMAT = ascii
957PS_LINE_CAP = round
958PS_LINE_JOIN = miter
959PS_MITER_LIMIT = 35
960PS_VERBOSE = FALSE
961TRANSPARENCY = 0
962#-------- I/O Format Parameters -------------
963D_FORMAT = %.12lg
964FIELD_DELIMITER = tab
965GRIDFILE_FORMAT = nf
966GRIDFILE_SHORTHAND = FALSE
967INPUT_CLOCK_FORMAT = hh:mm:ss
968INPUT_DATE_FORMAT = yyyy-mm-dd
969IO_HEADER = FALSE
970N_HEADER_RECS = 1
971NAN_RECORDS = pass
972OUTPUT_CLOCK_FORMAT = hh:mm:ss
973OUTPUT_DATE_FORMAT = yyyy-mm-dd
974OUTPUT_DEGREE_FORMAT = D
975XY_TOGGLE = FALSE
976#-------- Projection Parameters -------------
977ELLIPSOID = WGS-84
978MAP_SCALE_FACTOR = default
979MEASURE_UNIT = inch
980#-------- Calendar/Time Parameters ----------
981TIME_FORMAT_PRIMARY = full
982TIME_FORMAT_SECONDARY = full
983TIME_EPOCH = 2000-01-01T00:00:00
984TIME_IS_INTERVAL = OFF
985TIME_INTERVAL_FRACTION = 0.5
986TIME_LANGUAGE = us
987TIME_UNIT = d
988TIME_WEEK_START = Sunday
989Y2K_OFFSET_YEAR = 1950
990#-------- Miscellaneous Parameters ----------
991HISTORY = TRUE
992INTERPOLANT = akima
993LINE_STEP = 0.01i
994VECTOR_SHAPE = 0
995VERBOSE = FALSE
996'''
998_gmt_defaults_by_version['5.1.2'] = r'''
999#
1000# GMT 5.1.2 Defaults file
1001# vim:sw=8:ts=8:sts=8
1002# $Revision: 13836 $
1003# $LastChangedDate: 2014-12-20 03:45:42 -1000 (Sat, 20 Dec 2014) $
1004#
1005# COLOR Parameters
1006#
1007COLOR_BACKGROUND = black
1008COLOR_FOREGROUND = white
1009COLOR_NAN = 127.5
1010COLOR_MODEL = none
1011COLOR_HSV_MIN_S = 1
1012COLOR_HSV_MAX_S = 0.1
1013COLOR_HSV_MIN_V = 0.3
1014COLOR_HSV_MAX_V = 1
1015#
1016# DIR Parameters
1017#
1018DIR_DATA =
1019DIR_DCW =
1020DIR_GSHHG =
1021#
1022# FONT Parameters
1023#
1024FONT_ANNOT_PRIMARY = 14p,Helvetica,black
1025FONT_ANNOT_SECONDARY = 16p,Helvetica,black
1026FONT_LABEL = 14p,Helvetica,black
1027FONT_LOGO = 8p,Helvetica,black
1028FONT_TITLE = 24p,Helvetica,black
1029#
1030# FORMAT Parameters
1031#
1032FORMAT_CLOCK_IN = hh:mm:ss
1033FORMAT_CLOCK_OUT = hh:mm:ss
1034FORMAT_CLOCK_MAP = hh:mm:ss
1035FORMAT_DATE_IN = yyyy-mm-dd
1036FORMAT_DATE_OUT = yyyy-mm-dd
1037FORMAT_DATE_MAP = yyyy-mm-dd
1038FORMAT_GEO_OUT = D
1039FORMAT_GEO_MAP = ddd:mm:ss
1040FORMAT_FLOAT_OUT = %.12g
1041FORMAT_FLOAT_MAP = %.12g
1042FORMAT_TIME_PRIMARY_MAP = full
1043FORMAT_TIME_SECONDARY_MAP = full
1044FORMAT_TIME_STAMP = %Y %b %d %H:%M:%S
1045#
1046# GMT Miscellaneous Parameters
1047#
1048GMT_COMPATIBILITY = 4
1049GMT_CUSTOM_LIBS =
1050GMT_EXTRAPOLATE_VAL = NaN
1051GMT_FFT = auto
1052GMT_HISTORY = true
1053GMT_INTERPOLANT = akima
1054GMT_TRIANGULATE = Shewchuk
1055GMT_VERBOSE = compat
1056GMT_LANGUAGE = us
1057#
1058# I/O Parameters
1059#
1060IO_COL_SEPARATOR = tab
1061IO_GRIDFILE_FORMAT = nf
1062IO_GRIDFILE_SHORTHAND = false
1063IO_HEADER = false
1064IO_N_HEADER_RECS = 0
1065IO_NAN_RECORDS = pass
1066IO_NC4_CHUNK_SIZE = auto
1067IO_NC4_DEFLATION_LEVEL = 3
1068IO_LONLAT_TOGGLE = false
1069IO_SEGMENT_MARKER = >
1070#
1071# MAP Parameters
1072#
1073MAP_ANNOT_MIN_ANGLE = 20
1074MAP_ANNOT_MIN_SPACING = 0p
1075MAP_ANNOT_OBLIQUE = 1
1076MAP_ANNOT_OFFSET_PRIMARY = 0.075i
1077MAP_ANNOT_OFFSET_SECONDARY = 0.075i
1078MAP_ANNOT_ORTHO = we
1079MAP_DEFAULT_PEN = default,black
1080MAP_DEGREE_SYMBOL = ring
1081MAP_FRAME_AXES = WESNZ
1082MAP_FRAME_PEN = thicker,black
1083MAP_FRAME_TYPE = fancy
1084MAP_FRAME_WIDTH = 5p
1085MAP_GRID_CROSS_SIZE_PRIMARY = 0p
1086MAP_GRID_CROSS_SIZE_SECONDARY = 0p
1087MAP_GRID_PEN_PRIMARY = default,black
1088MAP_GRID_PEN_SECONDARY = thinner,black
1089MAP_LABEL_OFFSET = 0.1944i
1090MAP_LINE_STEP = 0.75p
1091MAP_LOGO = false
1092MAP_LOGO_POS = BL/-54p/-54p
1093MAP_ORIGIN_X = 1i
1094MAP_ORIGIN_Y = 1i
1095MAP_POLAR_CAP = 85/90
1096MAP_SCALE_HEIGHT = 5p
1097MAP_TICK_LENGTH_PRIMARY = 5p/2.5p
1098MAP_TICK_LENGTH_SECONDARY = 15p/3.75p
1099MAP_TICK_PEN_PRIMARY = thinner,black
1100MAP_TICK_PEN_SECONDARY = thinner,black
1101MAP_TITLE_OFFSET = 14p
1102MAP_VECTOR_SHAPE = 0
1103#
1104# Projection Parameters
1105#
1106PROJ_AUX_LATITUDE = authalic
1107PROJ_ELLIPSOID = WGS-84
1108PROJ_LENGTH_UNIT = cm
1109PROJ_MEAN_RADIUS = authalic
1110PROJ_SCALE_FACTOR = default
1111#
1112# PostScript Parameters
1113#
1114PS_CHAR_ENCODING = ISOLatin1+
1115PS_COLOR_MODEL = rgb
1116PS_COMMENTS = false
1117PS_IMAGE_COMPRESS = deflate,5
1118PS_LINE_CAP = butt
1119PS_LINE_JOIN = miter
1120PS_MITER_LIMIT = 35
1121PS_MEDIA = a4
1122PS_PAGE_COLOR = white
1123PS_PAGE_ORIENTATION = portrait
1124PS_SCALE_X = 1
1125PS_SCALE_Y = 1
1126PS_TRANSPARENCY = Normal
1127#
1128# Calendar/Time Parameters
1129#
1130TIME_EPOCH = 1970-01-01T00:00:00
1131TIME_IS_INTERVAL = off
1132TIME_INTERVAL_FRACTION = 0.5
1133TIME_UNIT = s
1134TIME_WEEK_START = Monday
1135TIME_Y2K_OFFSET_YEAR = 1950
1136'''
1139def get_gmt_version(gmtdefaultsbinary, gmthomedir=None):
1140 args = [gmtdefaultsbinary]
1142 environ = os.environ.copy()
1143 environ['GMTHOME'] = gmthomedir or ''
1145 p = subprocess.Popen(
1146 args,
1147 stdout=subprocess.PIPE,
1148 stderr=subprocess.PIPE,
1149 env=environ)
1151 (stdout, stderr) = p.communicate()
1152 m = re.search(br'(\d+(\.\d+)*)', stderr) \
1153 or re.search(br'# GMT (\d+(\.\d+)*)', stdout)
1155 if not m:
1156 raise GMTInstallationProblem(
1157 "Can't extract version number from output of %s."
1158 % gmtdefaultsbinary)
1160 return str(m.group(1).decode('ascii'))
1163def detect_gmt_installations():
1165 installations = {}
1166 errmesses = []
1168 # GMT 4.x:
1169 try:
1170 p = subprocess.Popen(
1171 ['GMT'],
1172 stdout=subprocess.PIPE,
1173 stderr=subprocess.PIPE)
1175 (stdout, stderr) = p.communicate()
1177 m = re.search(br'Version\s+(\d+(\.\d+)*)', stderr, re.M)
1178 if not m:
1179 raise GMTInstallationProblem(
1180 "Can't get version number from output of GMT.")
1182 version = str(m.group(1).decode('ascii'))
1183 if version[0] != '5':
1185 m = re.search(br'^\s+executables\s+(.+)$', stderr, re.M)
1186 if not m:
1187 raise GMTInstallationProblem(
1188 "Can't extract executables dir from output of GMT.")
1190 gmtbin = str(m.group(1).decode('ascii'))
1192 m = re.search(br'^\s+shared data\s+(.+)$', stderr, re.M)
1193 if not m:
1194 raise GMTInstallationProblem(
1195 "Can't extract shared dir from output of GMT.")
1197 gmtshare = str(m.group(1).decode('ascii'))
1198 if not gmtshare.endswith('/share'):
1199 raise GMTInstallationProblem(
1200 "Can't determine GMTHOME from output of GMT.")
1202 gmthome = gmtshare[:-6]
1204 installations[version] = {
1205 'home': gmthome,
1206 'bin': gmtbin}
1208 except OSError as e:
1209 errmesses.append(('GMT', str(e)))
1211 try:
1212 version = str(subprocess.check_output(
1213 ['gmt', '--version']).strip().decode('ascii')).split('_')[0]
1214 gmtbin = str(subprocess.check_output(
1215 ['gmt', '--show-bindir']).strip().decode('ascii'))
1216 installations[version] = {
1217 'bin': gmtbin}
1219 except (OSError, subprocess.CalledProcessError) as e:
1220 errmesses.append(('gmt', str(e)))
1222 if not installations:
1223 s = []
1224 for (progname, errmess) in errmesses:
1225 s.append('Cannot start "%s" executable: %s' % (progname, errmess))
1227 raise GMTInstallationProblem(', '.join(s))
1229 return installations
1232def appropriate_defaults_version(version):
1233 avails = sorted(_gmt_defaults_by_version.keys(), key=key_version)
1234 for iavail, avail in enumerate(avails):
1235 if key_version(version) == key_version(avail):
1236 return version
1238 elif key_version(version) < key_version(avail):
1239 return avails[max(0, iavail-1)]
1241 return avails[-1]
1244def gmt_default_config(version):
1245 '''
1246 Get default GMT configuration dict for given version.
1247 '''
1249 xversion = appropriate_defaults_version(version)
1251 # if not version in _gmt_defaults_by_version:
1252 # raise GMTError('No GMT defaults for version %s found' % version)
1254 gmt_defaults = _gmt_defaults_by_version[xversion]
1256 d = {}
1257 for line in gmt_defaults.splitlines():
1258 sline = line.strip()
1259 if not sline or sline.startswith('#'):
1260 continue
1262 k, v = sline.split('=', 1)
1263 d[k.strip()] = v.strip()
1265 return d
1268def diff_defaults(v1, v2):
1269 d1 = gmt_default_config(v1)
1270 d2 = gmt_default_config(v2)
1271 for k in d1:
1272 if k not in d2:
1273 print('%s not in %s' % (k, v2))
1274 else:
1275 if d1[k] != d2[k]:
1276 print('%s %s = %s' % (v1, k, d1[k]))
1277 print('%s %s = %s' % (v2, k, d2[k]))
1279 for k in d2:
1280 if k not in d1:
1281 print('%s not in %s' % (k, v1))
1283# diff_defaults('4.5.2', '4.5.3')
1286def check_gmt_installation(installation):
1288 home_dir = installation.get('home', None)
1289 bin_dir = installation['bin']
1290 version = installation['version']
1292 for d in home_dir, bin_dir:
1293 if d is not None:
1294 if not os.path.exists(d):
1295 logging.error(('Directory does not exist: %s\n'
1296 'Check your GMT installation.') % d)
1298 major_version = version.split('.')[0]
1300 if major_version not in ['5', '6']:
1301 gmtdefaults = pjoin(bin_dir, 'gmtdefaults')
1303 versionfound = get_gmt_version(gmtdefaults, home_dir)
1305 if versionfound != version:
1306 raise GMTInstallationProblem((
1307 'Expected GMT version %s but found version %s.\n'
1308 '(Looking at output of %s)') % (
1309 version, versionfound, gmtdefaults))
1312def get_gmt_installation(version):
1313 setup_gmt_installations()
1314 if version != 'newest' and version not in _gmt_installations:
1315 logging.warn('GMT version %s not installed, taking version %s instead'
1316 % (version, newest_installed_gmt_version()))
1318 version = 'newest'
1320 if version == 'newest':
1321 version = newest_installed_gmt_version()
1323 installation = dict(_gmt_installations[version])
1325 return installation
1328def setup_gmt_installations():
1329 if not setup_gmt_installations.have_done:
1330 if not _gmt_installations:
1332 _gmt_installations.update(detect_gmt_installations())
1334 # store defaults as dicts into the gmt installations dicts
1335 for version, installation in _gmt_installations.items():
1336 installation['defaults'] = gmt_default_config(version)
1337 installation['version'] = version
1339 for installation in _gmt_installations.values():
1340 check_gmt_installation(installation)
1342 setup_gmt_installations.have_done = True
1345setup_gmt_installations.have_done = False
1347_paper_sizes_a = '''A0 2380 3368
1348 A1 1684 2380
1349 A2 1190 1684
1350 A3 842 1190
1351 A4 595 842
1352 A5 421 595
1353 A6 297 421
1354 A7 210 297
1355 A8 148 210
1356 A9 105 148
1357 A10 74 105
1358 B0 2836 4008
1359 B1 2004 2836
1360 B2 1418 2004
1361 B3 1002 1418
1362 B4 709 1002
1363 B5 501 709
1364 archA 648 864
1365 archB 864 1296
1366 archC 1296 1728
1367 archD 1728 2592
1368 archE 2592 3456
1369 flsa 612 936
1370 halfletter 396 612
1371 note 540 720
1372 letter 612 792
1373 legal 612 1008
1374 11x17 792 1224
1375 ledger 1224 792'''
1378_paper_sizes = {}
1381def setup_paper_sizes():
1382 if not _paper_sizes:
1383 for line in _paper_sizes_a.splitlines():
1384 k, w, h = line.split()
1385 _paper_sizes[k.lower()] = float(w), float(h)
1388def get_paper_size(k):
1389 setup_paper_sizes()
1390 return _paper_sizes[k.lower().rstrip('+')]
1393def all_paper_sizes():
1394 setup_paper_sizes()
1395 return _paper_sizes
1398def measure_unit(gmt_config):
1399 for k in ['MEASURE_UNIT', 'PROJ_LENGTH_UNIT']:
1400 if k in gmt_config:
1401 return gmt_config[k]
1403 raise GmtPyError('cannot get measure unit / proj length unit from config')
1406def paper_media(gmt_config):
1407 for k in ['PAPER_MEDIA', 'PS_MEDIA']:
1408 if k in gmt_config:
1409 return gmt_config[k]
1411 raise GmtPyError('cannot get paper media from config')
1414def page_orientation(gmt_config):
1415 for k in ['PAGE_ORIENTATION', 'PS_PAGE_ORIENTATION']:
1416 if k in gmt_config:
1417 return gmt_config[k]
1419 raise GmtPyError('cannot get paper orientation from config')
1422def make_bbox(width, height, gmt_config, margins=(0.8, 0.8, 0.8, 0.8)):
1424 leftmargin, topmargin, rightmargin, bottommargin = margins
1425 portrait = page_orientation(gmt_config).lower() == 'portrait'
1427 paper_size = get_paper_size(paper_media(gmt_config))
1428 if not portrait:
1429 paper_size = paper_size[1], paper_size[0]
1431 xoffset = (paper_size[0] - (width + leftmargin + rightmargin)) / \
1432 2.0 + leftmargin
1433 yoffset = (paper_size[1] - (height + topmargin + bottommargin)) / \
1434 2.0 + bottommargin
1436 if portrait:
1437 bb1 = int((xoffset - leftmargin))
1438 bb2 = int((yoffset - bottommargin))
1439 bb3 = bb1 + int((width+leftmargin+rightmargin))
1440 bb4 = bb2 + int((height+topmargin+bottommargin))
1441 else:
1442 bb1 = int((yoffset - topmargin))
1443 bb2 = int((xoffset - leftmargin))
1444 bb3 = bb1 + int((height+topmargin+bottommargin))
1445 bb4 = bb2 + int((width+leftmargin+rightmargin))
1447 return xoffset, yoffset, (bb1, bb2, bb3, bb4)
1450def gmtdefaults_as_text(version='newest'):
1452 '''
1453 Get the built-in gmtdefaults.
1454 '''
1456 if version not in _gmt_installations:
1457 logging.warn('GMT version %s not installed, taking version %s instead'
1458 % (version, newest_installed_gmt_version()))
1459 version = 'newest'
1461 if version == 'newest':
1462 version = newest_installed_gmt_version()
1464 return _gmt_defaults_by_version[version]
1467def savegrd(x, y, z, filename, title=None, naming='xy'):
1468 '''
1469 Write COARDS compliant netcdf (grd) file.
1470 '''
1472 assert y.size, x.size == z.shape
1473 ny, nx = z.shape
1474 nc = netcdf_file(filename, 'w')
1475 assert naming in ('xy', 'lonlat')
1477 if naming == 'xy':
1478 kx, ky = 'x', 'y'
1479 else:
1480 kx, ky = 'lon', 'lat'
1482 nc.node_offset = 0
1483 if title is not None:
1484 nc.title = title
1486 nc.Conventions = 'COARDS/CF-1.0'
1487 nc.createDimension(kx, nx)
1488 nc.createDimension(ky, ny)
1490 xvar = nc.createVariable(kx, 'd', (kx,))
1491 yvar = nc.createVariable(ky, 'd', (ky,))
1492 if naming == 'xy':
1493 xvar.long_name = kx
1494 yvar.long_name = ky
1495 else:
1496 xvar.long_name = 'longitude'
1497 xvar.units = 'degrees_east'
1498 yvar.long_name = 'latitude'
1499 yvar.units = 'degrees_north'
1501 zvar = nc.createVariable('z', 'd', (ky, kx))
1503 xvar[:] = x.astype(num.float64)
1504 yvar[:] = y.astype(num.float64)
1505 zvar[:] = z.astype(num.float64)
1507 nc.close()
1510def to_array(var):
1511 arr = var[:].copy()
1512 if hasattr(var, 'scale_factor'):
1513 arr *= var.scale_factor
1515 if hasattr(var, 'add_offset'):
1516 arr += var.add_offset
1518 return arr
1521def loadgrd(filename):
1522 '''
1523 Read COARDS compliant netcdf (grd) file.
1524 '''
1526 nc = netcdf_file(filename, 'r')
1527 vkeys = list(nc.variables.keys())
1528 kx = 'x'
1529 ky = 'y'
1530 if 'lon' in vkeys:
1531 kx = 'lon'
1532 if 'lat' in vkeys:
1533 ky = 'lat'
1535 kz = 'z'
1536 if 'altitude' in vkeys:
1537 kz = 'altitude'
1539 x = to_array(nc.variables[kx])
1540 y = to_array(nc.variables[ky])
1541 z = to_array(nc.variables[kz])
1543 nc.close()
1544 return x, y, z
1547def centers_to_edges(asorted):
1548 return (asorted[1:] + asorted[:-1])/2.
1551def nvals(asorted):
1552 eps = (asorted[-1]-asorted[0])/asorted.size
1553 return num.sum(asorted[1:] - asorted[:-1] >= eps) + 1
1556def guess_vals(asorted):
1557 eps = (asorted[-1]-asorted[0])/asorted.size
1558 indis = num.nonzero(asorted[1:] - asorted[:-1] >= eps)[0]
1559 indis = num.concatenate((num.array([0]), indis+1,
1560 num.array([asorted.size])))
1561 asum = num.zeros(asorted.size+1)
1562 asum[1:] = num.cumsum(asorted)
1563 return (asum[indis[1:]] - asum[indis[:-1]]) / (indis[1:]-indis[:-1])
1566def blockmean(asorted, b):
1567 indis = num.nonzero(asorted[1:] - asorted[:-1])[0]
1568 indis = num.concatenate((num.array([0]), indis+1,
1569 num.array([asorted.size])))
1570 bsum = num.zeros(b.size+1)
1571 bsum[1:] = num.cumsum(b)
1572 return (
1573 asorted[indis[:-1]],
1574 (bsum[indis[1:]] - bsum[indis[:-1]]) / (indis[1:]-indis[:-1]))
1577def griddata_regular(x, y, z, xvals, yvals):
1578 nx, ny = xvals.size, yvals.size
1579 xindi = num.digitize(x, centers_to_edges(xvals))
1580 yindi = num.digitize(y, centers_to_edges(yvals))
1582 zindi = yindi*nx+xindi
1583 order = num.argsort(zindi)
1584 z = z[order]
1585 zindi = zindi[order]
1587 zindi, z = blockmean(zindi, z)
1588 znew = num.empty(nx*ny, dtype=float)
1589 znew[:] = num.nan
1590 znew[zindi] = z
1591 return znew.reshape(ny, nx)
1594def guess_field_size(x_sorted, y_sorted, z=None, mode=None):
1595 critical_fraction = 1./num.e - 0.014*3
1596 xs = x_sorted
1597 ys = y_sorted
1598 nxs, nys = nvals(xs), nvals(ys)
1599 if mode == 'nonrandom':
1600 return nxs, nys, 0
1601 elif xs.size == nxs*nys:
1602 # exact match
1603 return nxs, nys, 0
1604 elif nxs >= xs.size*critical_fraction and nys >= xs.size*critical_fraction:
1605 # possibly randomly sampled
1606 nxs = int(math.sqrt(xs.size))
1607 nys = nxs
1608 return nxs, nys, 2
1609 else:
1610 return nxs, nys, 1
1613def griddata_auto(x, y, z, mode=None):
1614 '''
1615 Grid tabular XYZ data by binning.
1617 This function does some extra work to guess the size of the grid. This
1618 should work fine if the input values are already defined on an rectilinear
1619 grid, even if data points are missing or duplicated. This routine also
1620 tries to detect a random distribution of input data and in that case
1621 creates a grid of size sqrt(N) x sqrt(N).
1623 The points do not have to be given in any particular order. Grid nodes
1624 without data are assigned the NaN value. If multiple data points map to the
1625 same grid node, their average is assigned to the grid node.
1626 '''
1628 x, y, z = [num.asarray(X) for X in (x, y, z)]
1629 assert x.size == y.size == z.size
1630 xs, ys = num.sort(x), num.sort(y)
1631 nx, ny, badness = guess_field_size(xs, ys, z, mode=mode)
1632 if badness <= 1:
1633 xf = guess_vals(xs)
1634 yf = guess_vals(ys)
1635 zf = griddata_regular(x, y, z, xf, yf)
1636 else:
1637 xf = num.linspace(xs[0], xs[-1], nx)
1638 yf = num.linspace(ys[0], ys[-1], ny)
1639 zf = griddata_regular(x, y, z, xf, yf)
1641 return xf, yf, zf
1644def tabledata(xf, yf, zf):
1645 assert yf.size, xf.size == zf.shape
1646 x = num.tile(xf, yf.size)
1647 y = num.repeat(yf, xf.size)
1648 z = zf.flatten()
1649 return x, y, z
1652def double1d(a):
1653 a2 = num.empty(a.size*2-1)
1654 a2[::2] = a
1655 a2[1::2] = (a[:-1] + a[1:])/2.
1656 return a2
1659def double2d(f):
1660 f2 = num.empty((f.shape[0]*2-1, f.shape[1]*2-1))
1661 f2[:, :] = num.nan
1662 f2[::2, ::2] = f
1663 f2[1::2, ::2] = (f[:-1, :] + f[1:, :])/2.
1664 f2[::2, 1::2] = (f[:, :-1] + f[:, 1:])/2.
1665 f2[1::2, 1::2] = (f[:-1, :-1] + f[1:, :-1] + f[:-1, 1:] + f[1:, 1:])/4.
1666 diag = f2[1::2, 1::2]
1667 diagA = (f[:-1, :-1] + f[1:, 1:]) / 2.
1668 diagB = (f[1:, :-1] + f[:-1, 1:]) / 2.
1669 f2[1::2, 1::2] = num.where(num.isnan(diag), diagA, diag)
1670 f2[1::2, 1::2] = num.where(num.isnan(diag), diagB, diag)
1671 return f2
1674def doublegrid(x, y, z):
1675 x2 = double1d(x)
1676 y2 = double1d(y)
1677 z2 = double2d(z)
1678 return x2, y2, z2
1681class Guru(object):
1682 '''
1683 Abstract base class providing template interpolation, accessible as
1684 attributes.
1686 Classes deriving from this one, have to implement a :py:meth:`get_params`
1687 method, which is called to get a dict to do ordinary
1688 ``"%(key)x"``-substitutions. The deriving class must also provide a dict
1689 with the templates.
1690 '''
1692 def __init__(self):
1693 self.templates = {}
1695 def fill(self, templates, **kwargs):
1696 params = self.get_params(**kwargs)
1697 strings = [t % params for t in templates]
1698 return strings
1700 # hand through templates dict
1701 def __getitem__(self, template_name):
1702 return self.templates[template_name]
1704 def __setitem__(self, template_name, template):
1705 self.templates[template_name] = template
1707 def __contains__(self, template_name):
1708 return template_name in self.templates
1710 def __iter__(self):
1711 return iter(self.templates)
1713 def __len__(self):
1714 return len(self.templates)
1716 def __delitem__(self, template_name):
1717 del self.templates[template_name]
1719 def _simple_fill(self, template_names, **kwargs):
1720 templates = [self.templates[n] for n in template_names]
1721 return self.fill(templates, **kwargs)
1723 def __getattr__(self, template_names):
1724 if [n for n in template_names if n not in self.templates]:
1725 raise AttributeError(template_names)
1727 def f(**kwargs):
1728 return self._simple_fill(template_names, **kwargs)
1730 return f
1733def nice_value(x):
1734 '''
1735 Round ``x`` to nice value.
1736 '''
1738 exp = 1.0
1739 sign = 1
1740 if x < 0.0:
1741 x = -x
1742 sign = -1
1743 while x >= 1.0:
1744 x /= 10.0
1745 exp *= 10.0
1746 while x < 0.1:
1747 x *= 10.0
1748 exp /= 10.0
1750 if x >= 0.75:
1751 return sign * 1.0 * exp
1752 if x >= 0.375:
1753 return sign * 0.5 * exp
1754 if x >= 0.225:
1755 return sign * 0.25 * exp
1756 if x >= 0.15:
1757 return sign * 0.2 * exp
1759 return sign * 0.1 * exp
1762class AutoScaler(object):
1763 '''
1764 Tunable 1D autoscaling based on data range.
1766 Instances of this class may be used to determine nice minima, maxima and
1767 increments for ax annotations, as well as suitable common exponents for
1768 notation.
1770 The autoscaling process is guided by the following public attributes:
1772 .. py:attribute:: approx_ticks
1774 Approximate number of increment steps (tickmarks) to generate.
1776 .. py:attribute:: mode
1778 Mode of operation: one of ``'auto'``, ``'min-max'``, ``'0-max'``,
1779 ``'min-0'``, ``'symmetric'`` or ``'off'``.
1781 ================ ==================================================
1782 mode description
1783 ================ ==================================================
1784 ``'auto'``: Look at data range and choose one of the choices
1785 below.
1786 ``'min-max'``: Output range is selected to include data range.
1787 ``'0-max'``: Output range shall start at zero and end at data
1788 max.
1789 ``'min-0'``: Output range shall start at data min and end at
1790 zero.
1791 ``'symmetric'``: Output range shall by symmetric by zero.
1792 ``'off'``: Similar to ``'min-max'``, but snap and space are
1793 disabled, such that the output range always
1794 exactly matches the data range.
1795 ================ ==================================================
1797 .. py:attribute:: exp
1799 If defined, override automatically determined exponent for notation
1800 by the given value.
1802 .. py:attribute:: snap
1804 If set to True, snap output range to multiples of increment. This
1805 parameter has no effect, if mode is set to ``'off'``.
1807 .. py:attribute:: inc
1809 If defined, override automatically determined tick increment by the
1810 given value.
1812 .. py:attribute:: space
1814 Add some padding to the range. The value given, is the fraction by
1815 which the output range is increased on each side. If mode is
1816 ``'0-max'`` or ``'min-0'``, the end at zero is kept fixed at zero.
1817 This parameter has no effect if mode is set to ``'off'``.
1819 .. py:attribute:: exp_factor
1821 Exponent of notation is chosen to be a multiple of this value.
1823 .. py:attribute:: no_exp_interval:
1825 Range of exponent, for which no exponential notation is allowed.
1827 '''
1829 def __init__(
1830 self,
1831 approx_ticks=7.0,
1832 mode='auto',
1833 exp=None,
1834 snap=False,
1835 inc=None,
1836 space=0.0,
1837 exp_factor=3,
1838 no_exp_interval=(-3, 5)):
1840 '''
1841 Create new AutoScaler instance.
1843 The parameters are described in the AutoScaler documentation.
1844 '''
1846 self.approx_ticks = approx_ticks
1847 self.mode = mode
1848 self.exp = exp
1849 self.snap = snap
1850 self.inc = inc
1851 self.space = space
1852 self.exp_factor = exp_factor
1853 self.no_exp_interval = no_exp_interval
1855 def make_scale(self, data_range, override_mode=None):
1857 '''
1858 Get nice minimum, maximum and increment for given data range.
1860 Returns ``(minimum, maximum, increment)`` or ``(maximum, minimum,
1861 -increment)``, depending on whether data_range is ``(data_min,
1862 data_max)`` or ``(data_max, data_min)``. If ``override_mode`` is
1863 defined, the mode attribute is temporarily overridden by the given
1864 value.
1865 '''
1867 data_min = min(data_range)
1868 data_max = max(data_range)
1870 is_reverse = (data_range[0] > data_range[1])
1872 a = self.mode
1873 if self.mode == 'auto':
1874 a = self.guess_autoscale_mode(data_min, data_max)
1876 if override_mode is not None:
1877 a = override_mode
1879 mi, ma = 0, 0
1880 if a == 'off':
1881 mi, ma = data_min, data_max
1882 elif a == '0-max':
1883 mi = 0.0
1884 if data_max > 0.0:
1885 ma = data_max
1886 else:
1887 ma = 1.0
1888 elif a == 'min-0':
1889 ma = 0.0
1890 if data_min < 0.0:
1891 mi = data_min
1892 else:
1893 mi = -1.0
1894 elif a == 'min-max':
1895 mi, ma = data_min, data_max
1896 elif a == 'symmetric':
1897 m = max(abs(data_min), abs(data_max))
1898 mi = -m
1899 ma = m
1901 nmi = mi
1902 if (mi != 0. or a == 'min-max') and a != 'off':
1903 nmi = mi - self.space*(ma-mi)
1905 nma = ma
1906 if (ma != 0. or a == 'min-max') and a != 'off':
1907 nma = ma + self.space*(ma-mi)
1909 mi, ma = nmi, nma
1911 if mi == ma and a != 'off':
1912 mi -= 1.0
1913 ma += 1.0
1915 # make nice tick increment
1916 if self.inc is not None:
1917 inc = self.inc
1918 else:
1919 if self.approx_ticks > 0.:
1920 inc = nice_value((ma-mi) / self.approx_ticks)
1921 else:
1922 inc = nice_value((ma-mi)*10.)
1924 if inc == 0.0:
1925 inc = 1.0
1927 # snap min and max to ticks if this is wanted
1928 if self.snap and a != 'off':
1929 ma = inc * math.ceil(ma/inc)
1930 mi = inc * math.floor(mi/inc)
1932 if is_reverse:
1933 return ma, mi, -inc
1934 else:
1935 return mi, ma, inc
1937 def make_exp(self, x):
1938 '''
1939 Get nice exponent for notation of ``x``.
1941 For ax annotations, give tick increment as ``x``.
1942 '''
1944 if self.exp is not None:
1945 return self.exp
1947 x = abs(x)
1948 if x == 0.0:
1949 return 0
1951 if 10**self.no_exp_interval[0] <= x <= 10**self.no_exp_interval[1]:
1952 return 0
1954 return math.floor(math.log10(x)/self.exp_factor)*self.exp_factor
1956 def guess_autoscale_mode(self, data_min, data_max):
1957 '''
1958 Guess mode of operation, based on data range.
1960 Used to map ``'auto'`` mode to ``'0-max'``, ``'min-0'``, ``'min-max'``
1961 or ``'symmetric'``.
1962 '''
1964 a = 'min-max'
1965 if data_min >= 0.0:
1966 if data_min < data_max/2.:
1967 a = '0-max'
1968 else:
1969 a = 'min-max'
1970 if data_max <= 0.0:
1971 if data_max > data_min/2.:
1972 a = 'min-0'
1973 else:
1974 a = 'min-max'
1975 if data_min < 0.0 and data_max > 0.0:
1976 if abs((abs(data_max)-abs(data_min)) /
1977 (abs(data_max)+abs(data_min))) < 0.5:
1978 a = 'symmetric'
1979 else:
1980 a = 'min-max'
1981 return a
1984class Ax(AutoScaler):
1985 '''
1986 Ax description with autoscaling capabilities.
1988 The ax is described by the :py:class:`AutoScaler` public attributes, plus
1989 the following additional attributes (with default values given in
1990 paranthesis):
1992 .. py:attribute:: label
1994 Ax label (without unit).
1996 .. py:attribute:: unit
1998 Physical unit of the data attached to this ax.
2000 .. py:attribute:: scaled_unit
2002 (see below)
2004 .. py:attribute:: scaled_unit_factor
2006 Scaled physical unit and factor between unit and scaled_unit so that
2008 unit = scaled_unit_factor x scaled_unit.
2010 (E.g. if unit is 'm' and data is in the range of nanometers, you may
2011 want to set the scaled_unit to 'nm' and the scaled_unit_factor to
2012 1e9.)
2014 .. py:attribute:: limits
2016 If defined, fix range of ax to limits=(min,max).
2018 .. py:attribute:: masking
2020 If true and if there is a limit on the ax, while calculating ranges,
2021 the data points are masked such that data points outside of this axes
2022 limits are not used to determine the range of another dependant ax.
2024 '''
2026 def __init__(self, label='', unit='', scaled_unit_factor=1.,
2027 scaled_unit='', limits=None, masking=True, **kwargs):
2029 AutoScaler.__init__(self, **kwargs)
2030 self.label = label
2031 self.unit = unit
2032 self.scaled_unit_factor = scaled_unit_factor
2033 self.scaled_unit = scaled_unit
2034 self.limits = limits
2035 self.masking = masking
2037 def label_str(self, exp, unit):
2038 '''
2039 Get label string including the unit and multiplier.
2040 '''
2042 slabel, sunit, sexp = '', '', ''
2043 if self.label:
2044 slabel = self.label
2046 if unit or exp != 0:
2047 if exp != 0:
2048 sexp = '\\327 10@+%i@+' % exp
2049 sunit = '[ %s %s ]' % (sexp, unit)
2050 else:
2051 sunit = '[ %s ]' % unit
2053 p = []
2054 if slabel:
2055 p.append(slabel)
2057 if sunit:
2058 p.append(sunit)
2060 return ' '.join(p)
2062 def make_params(self, data_range, ax_projection=False, override_mode=None,
2063 override_scaled_unit_factor=None):
2065 '''
2066 Get minimum, maximum, increment and label string for ax display.'
2068 Returns minimum, maximum, increment and label string including unit and
2069 multiplier for given data range.
2071 If ``ax_projection`` is True, values suitable to be displayed on the ax
2072 are returned, e.g. min, max and inc are returned in scaled units.
2073 Otherwise the values are returned in the original units, without any
2074 scaling applied.
2075 '''
2077 sf = self.scaled_unit_factor
2079 if override_scaled_unit_factor is not None:
2080 sf = override_scaled_unit_factor
2082 dr_scaled = [sf*x for x in data_range]
2084 mi, ma, inc = self.make_scale(dr_scaled, override_mode=override_mode)
2085 if self.inc is not None:
2086 inc = self.inc*sf
2088 if ax_projection:
2089 exp = self.make_exp(inc)
2090 if sf == 1. and override_scaled_unit_factor is None:
2091 unit = self.unit
2092 else:
2093 unit = self.scaled_unit
2094 label = self.label_str(exp, unit)
2095 return mi/10**exp, ma/10**exp, inc/10**exp, label
2096 else:
2097 label = self.label_str(0, self.unit)
2098 return mi/sf, ma/sf, inc/sf, label
2101class ScaleGuru(Guru):
2103 '''
2104 2D/3D autoscaling and ax annotation facility.
2106 Instances of this class provide automatic determination of plot ranges,
2107 tick increments and scaled annotations, as well as label/unit handling. It
2108 can in particular be used to automatically generate the -R and -B option
2109 arguments, which are required for most GMT commands.
2111 It extends the functionality of the :py:class:`Ax` and
2112 :py:class:`AutoScaler` classes at the level, where it can not be handled
2113 anymore by looking at a single dimension of the dataset's data, e.g.:
2115 * The ability to impose a fixed aspect ratio between two axes.
2117 * Recalculation of data range on non-limited axes, when there are
2118 limits imposed on other axes.
2120 '''
2122 def __init__(self, data_tuples=None, axes=None, aspect=None,
2123 percent_interval=None, copy_from=None):
2125 Guru.__init__(self)
2127 if copy_from:
2128 self.templates = copy.deepcopy(copy_from.templates)
2129 self.axes = copy.deepcopy(copy_from.axes)
2130 self.data_ranges = copy.deepcopy(copy_from.data_ranges)
2131 self.aspect = copy_from.aspect
2133 if percent_interval is not None:
2134 from scipy.stats import scoreatpercentile as scap
2136 self.templates = dict(
2137 R='-R%(xmin)g/%(xmax)g/%(ymin)g/%(ymax)g',
2138 B='-B%(xinc)g:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen',
2139 T='-T%(zmin)g/%(zmax)g/%(zinc)g')
2141 maxdim = 2
2142 if data_tuples:
2143 maxdim = max(maxdim, max([len(dt) for dt in data_tuples]))
2144 else:
2145 if axes:
2146 maxdim = len(axes)
2147 data_tuples = [([],) * maxdim]
2148 if axes is not None:
2149 self.axes = axes
2150 else:
2151 self.axes = [Ax() for i in range(maxdim)]
2153 # sophisticated data-range calculation
2154 data_ranges = [None] * maxdim
2155 for dt_ in data_tuples:
2156 dt = num.asarray(dt_)
2157 in_range = True
2158 for ax, x in zip(self.axes, dt):
2159 if ax.limits and ax.masking:
2160 ax_limits = list(ax.limits)
2161 if ax_limits[0] is None:
2162 ax_limits[0] = -num.inf
2163 if ax_limits[1] is None:
2164 ax_limits[1] = num.inf
2165 in_range = num.logical_and(
2166 in_range,
2167 num.logical_and(ax_limits[0] <= x, x <= ax_limits[1]))
2169 for i, ax, x in zip(range(maxdim), self.axes, dt):
2171 if not ax.limits or None in ax.limits:
2172 if len(x) >= 1:
2173 if in_range is not True:
2174 xmasked = num.where(in_range, x, num.NaN)
2175 if percent_interval is None:
2176 range_this = (
2177 num.nanmin(xmasked),
2178 num.nanmax(xmasked))
2179 else:
2180 xmasked_finite = num.compress(
2181 num.isfinite(xmasked), xmasked)
2182 range_this = (
2183 scap(xmasked_finite,
2184 (100.-percent_interval)/2.),
2185 scap(xmasked_finite,
2186 100.-(100.-percent_interval)/2.))
2187 else:
2188 if percent_interval is None:
2189 range_this = num.nanmin(x), num.nanmax(x)
2190 else:
2191 xmasked_finite = num.compress(
2192 num.isfinite(xmasked), xmasked)
2193 range_this = (
2194 scap(xmasked_finite,
2195 (100.-percent_interval)/2.),
2196 scap(xmasked_finite,
2197 100.-(100.-percent_interval)/2.))
2198 else:
2199 range_this = (0., 1.)
2201 if ax.limits:
2202 if ax.limits[0] is not None:
2203 range_this = ax.limits[0], max(ax.limits[0],
2204 range_this[1])
2206 if ax.limits[1] is not None:
2207 range_this = min(ax.limits[1],
2208 range_this[0]), ax.limits[1]
2210 else:
2211 range_this = ax.limits
2213 if data_ranges[i] is None and range_this[0] <= range_this[1]:
2214 data_ranges[i] = range_this
2215 else:
2216 mi, ma = range_this
2217 if data_ranges[i] is not None:
2218 mi = min(data_ranges[i][0], mi)
2219 ma = max(data_ranges[i][1], ma)
2221 data_ranges[i] = (mi, ma)
2223 for i in range(len(data_ranges)):
2224 if data_ranges[i] is None or not (
2225 num.isfinite(data_ranges[i][0])
2226 and num.isfinite(data_ranges[i][1])):
2228 data_ranges[i] = (0., 1.)
2230 self.data_ranges = data_ranges
2231 self.aspect = aspect
2233 def copy(self):
2234 return ScaleGuru(copy_from=self)
2236 def get_params(self, ax_projection=False):
2238 '''
2239 Get dict with output parameters.
2241 For each data dimension, ax minimum, maximum, increment and a label
2242 string (including unit and exponential factor) are determined. E.g. in
2243 for the first dimension the output dict will contain the keys
2244 ``'xmin'``, ``'xmax'``, ``'xinc'``, and ``'xlabel'``.
2246 Normally, values corresponding to the scaling of the raw data are
2247 produced, but if ``ax_projection`` is ``True``, values which are
2248 suitable to be printed on the axes are returned. This means that in the
2249 latter case, the :py:attr:`Ax.scaled_unit` and
2250 :py:attr:`Ax.scaled_unit_factor` attributes as set on the axes are
2251 respected and that a common 10^x factor is factored out and put to the
2252 label string.
2253 '''
2255 xmi, xma, xinc, xlabel = self.axes[0].make_params(
2256 self.data_ranges[0], ax_projection)
2257 ymi, yma, yinc, ylabel = self.axes[1].make_params(
2258 self.data_ranges[1], ax_projection)
2259 if len(self.axes) > 2:
2260 zmi, zma, zinc, zlabel = self.axes[2].make_params(
2261 self.data_ranges[2], ax_projection)
2263 # enforce certain aspect, if needed
2264 if self.aspect is not None:
2265 xwid = xma-xmi
2266 ywid = yma-ymi
2267 if ywid < xwid*self.aspect:
2268 ymi -= (xwid*self.aspect - ywid)*0.5
2269 yma += (xwid*self.aspect - ywid)*0.5
2270 ymi, yma, yinc, ylabel = self.axes[1].make_params(
2271 (ymi, yma), ax_projection, override_mode='off',
2272 override_scaled_unit_factor=1.)
2274 elif xwid < ywid/self.aspect:
2275 xmi -= (ywid/self.aspect - xwid)*0.5
2276 xma += (ywid/self.aspect - xwid)*0.5
2277 xmi, xma, xinc, xlabel = self.axes[0].make_params(
2278 (xmi, xma), ax_projection, override_mode='off',
2279 override_scaled_unit_factor=1.)
2281 params = dict(xmin=xmi, xmax=xma, xinc=xinc, xlabel=xlabel,
2282 ymin=ymi, ymax=yma, yinc=yinc, ylabel=ylabel)
2283 if len(self.axes) > 2:
2284 params.update(dict(zmin=zmi, zmax=zma, zinc=zinc, zlabel=zlabel))
2286 return params
2289class GumSpring(object):
2291 '''
2292 Sizing policy implementing a minimal size, plus a desire to grow.
2293 '''
2295 def __init__(self, minimal=None, grow=None):
2296 self.minimal = minimal
2297 if grow is None:
2298 if minimal is None:
2299 self.grow = 1.0
2300 else:
2301 self.grow = 0.0
2302 else:
2303 self.grow = grow
2304 self.value = 1.0
2306 def get_minimal(self):
2307 if self.minimal is not None:
2308 return self.minimal
2309 else:
2310 return 0.0
2312 def get_grow(self):
2313 return self.grow
2315 def set_value(self, value):
2316 self.value = value
2318 def get_value(self):
2319 return self.value
2322def distribute(sizes, grows, space):
2323 sizes = list(sizes)
2324 gsum = sum(grows)
2325 if gsum > 0.0:
2326 for i in range(len(sizes)):
2327 sizes[i] += space*grows[i]/gsum
2328 return sizes
2331class Widget(Guru):
2333 '''
2334 Base class of the gmtpy layout system.
2336 The Widget class provides the basic functionality for the nesting and
2337 placing of elements on the output page, and maintains the sizing policies
2338 of each element. Each of the layouts defined in gmtpy is itself a Widget.
2340 Sizing of the widget is controlled by :py:meth:`get_min_size` and
2341 :py:meth:`get_grow` which should be overloaded in derived classes. The
2342 basic behaviour of a Widget instance is to have a vertical and a horizontal
2343 minimum size which default to zero, as well as a vertical and a horizontal
2344 desire to grow, represented by floats, which default to 1.0. Additionally
2345 an aspect ratio constraint may be imposed on the Widget.
2347 After layouting, the widget provides its width, height, x-offset and
2348 y-offset in various ways. Via the Guru interface (see :py:class:`Guru`
2349 class), templates for the -X, -Y and -J option arguments used by GMT
2350 arguments are provided. The defaults are suitable for plotting of linear
2351 (-JX) plots. Other projections can be selected by giving an appropriate 'J'
2352 template, or by manual construction of the -J option, e.g. by utilizing the
2353 :py:meth:`width` and :py:meth:`height` methods. The :py:meth:`bbox` method
2354 can be used to create a PostScript bounding box from the widgets border,
2355 e.g. for use in the :py:meth:`save` method of :py:class:`GMT` instances.
2357 The convention is, that all sizes are given in PostScript points.
2358 Conversion factors are provided as constants :py:const:`inch` and
2359 :py:const:`cm` in the gmtpy module.
2360 '''
2362 def __init__(self, horizontal=None, vertical=None, parent=None):
2364 '''
2365 Create new widget.
2366 '''
2368 Guru.__init__(self)
2370 self.templates = dict(
2371 X='-Xa%(xoffset)gp',
2372 Y='-Ya%(yoffset)gp',
2373 J='-JX%(width)gp/%(height)gp')
2375 if horizontal is None:
2376 self.horizontal = GumSpring()
2377 else:
2378 self.horizontal = horizontal
2380 if vertical is None:
2381 self.vertical = GumSpring()
2382 else:
2383 self.vertical = vertical
2385 self.aspect = None
2386 self.parent = parent
2387 self.dirty = True
2389 def set_parent(self, parent):
2391 '''
2392 Set the parent widget.
2394 This method should not be called directly. The :py:meth:`set_widget`
2395 methods are responsible for calling this.
2396 '''
2398 self.parent = parent
2399 self.dirtyfy()
2401 def get_parent(self):
2403 '''
2404 Get the widgets parent widget.
2405 '''
2407 return self.parent
2409 def get_root(self):
2411 '''
2412 Get the root widget in the layout hierarchy.
2413 '''
2415 if self.parent is not None:
2416 return self.get_parent()
2417 else:
2418 return self
2420 def set_horizontal(self, minimal=None, grow=None):
2422 '''
2423 Set the horizontal sizing policy of the Widget.
2426 :param minimal: new minimal width of the widget
2427 :param grow: new horizontal grow disire of the widget
2428 '''
2430 self.horizontal = GumSpring(minimal, grow)
2431 self.dirtyfy()
2433 def get_horizontal(self):
2434 return self.horizontal.get_minimal(), self.horizontal.get_grow()
2436 def set_vertical(self, minimal=None, grow=None):
2438 '''
2439 Set the horizontal sizing policy of the Widget.
2441 :param minimal: new minimal height of the widget
2442 :param grow: new vertical grow disire of the widget
2443 '''
2445 self.vertical = GumSpring(minimal, grow)
2446 self.dirtyfy()
2448 def get_vertical(self):
2449 return self.vertical.get_minimal(), self.vertical.get_grow()
2451 def set_aspect(self, aspect=None):
2453 '''
2454 Set aspect constraint on the widget.
2456 The aspect is given as height divided by width.
2457 '''
2459 self.aspect = aspect
2460 self.dirtyfy()
2462 def set_policy(self, minimal=(None, None), grow=(None, None), aspect=None):
2464 '''
2465 Shortcut to set sizing and aspect constraints in a single method
2466 call.
2467 '''
2469 self.set_horizontal(minimal[0], grow[0])
2470 self.set_vertical(minimal[1], grow[1])
2471 self.set_aspect(aspect)
2473 def get_policy(self):
2474 mh, gh = self.get_horizontal()
2475 mv, gv = self.get_vertical()
2476 return (mh, mv), (gh, gv), self.aspect
2478 def legalize(self, size, offset):
2480 '''
2481 Get legal size for widget.
2483 Returns: (new_size, new_offset)
2485 Given a box as ``size`` and ``offset``, return ``new_size`` and
2486 ``new_offset``, such that the widget's sizing and aspect constraints
2487 are fullfilled. The returned box is centered on the given input box.
2488 '''
2490 sh, sv = size
2491 oh, ov = offset
2492 shs, svs = Widget.get_min_size(self)
2493 ghs, gvs = Widget.get_grow(self)
2495 if ghs == 0.0:
2496 oh += (sh-shs)/2.
2497 sh = shs
2499 if gvs == 0.0:
2500 ov += (sv-svs)/2.
2501 sv = svs
2503 if self.aspect is not None:
2504 if sh > sv/self.aspect:
2505 oh += (sh-sv/self.aspect)/2.
2506 sh = sv/self.aspect
2507 if sv > sh*self.aspect:
2508 ov += (sv-sh*self.aspect)/2.
2509 sv = sh*self.aspect
2511 return (sh, sv), (oh, ov)
2513 def get_min_size(self):
2515 '''
2516 Get minimum size of widget.
2518 Used by the layout managers. Should be overloaded in derived classes.
2519 '''
2521 mh, mv = self.horizontal.get_minimal(), self.vertical.get_minimal()
2522 if self.aspect is not None:
2523 if mv == 0.0:
2524 return mh, mh*self.aspect
2525 elif mh == 0.0:
2526 return mv/self.aspect, mv
2527 return mh, mv
2529 def get_grow(self):
2531 '''
2532 Get widget's desire to grow.
2534 Used by the layout managers. Should be overloaded in derived classes.
2535 '''
2537 return self.horizontal.get_grow(), self.vertical.get_grow()
2539 def set_size(self, size, offset):
2541 '''
2542 Set the widget's current size.
2544 Should not be called directly. It is the layout manager's
2545 responsibility to call this.
2546 '''
2548 (sh, sv), inner_offset = self.legalize(size, offset)
2549 self.offset = inner_offset
2550 self.horizontal.set_value(sh)
2551 self.vertical.set_value(sv)
2552 self.dirty = False
2554 def __str__(self):
2556 def indent(ind, str):
2557 return ('\n'+ind).join(str.splitlines())
2558 size, offset = self.get_size()
2559 s = "%s (%g x %g) (%g, %g)\n" % ((self.__class__,) + size + offset)
2560 children = self.get_children()
2561 if children:
2562 s += '\n'.join([' ' + indent(' ', str(c)) for c in children])
2563 return s
2565 def policies_debug_str(self):
2567 def indent(ind, str):
2568 return ('\n'+ind).join(str.splitlines())
2569 mins, grows, aspect = self.get_policy()
2570 s = "%s: minimum=(%s, %s), grow=(%s, %s), aspect=%s\n" % (
2571 (self.__class__,) + mins+grows+(aspect,))
2573 children = self.get_children()
2574 if children:
2575 s += '\n'.join([' ' + indent(
2576 ' ', c.policies_debug_str()) for c in children])
2577 return s
2579 def get_corners(self, descend=False):
2581 '''
2582 Get coordinates of the corners of the widget.
2584 Returns list with coordinate tuples.
2586 If ``descend`` is True, the returned list will contain corner
2587 coordinates of all sub-widgets.
2588 '''
2590 self.do_layout()
2591 (sh, sv), (oh, ov) = self.get_size()
2592 corners = [(oh, ov), (oh+sh, ov), (oh+sh, ov+sv), (oh, ov+sv)]
2593 if descend:
2594 for child in self.get_children():
2595 corners.extend(child.get_corners(descend=True))
2596 return corners
2598 def get_sizes(self):
2600 '''
2601 Get sizes of this widget and all it's children.
2603 Returns a list with size tuples.
2604 '''
2605 self.do_layout()
2606 sizes = [self.get_size()]
2607 for child in self.get_children():
2608 sizes.extend(child.get_sizes())
2609 return sizes
2611 def do_layout(self):
2613 '''
2614 Triggers layouting of the widget hierarchy, if needed.
2615 '''
2617 if self.parent is not None:
2618 return self.parent.do_layout()
2620 if not self.dirty:
2621 return
2623 sh, sv = self.get_min_size()
2624 gh, gv = self.get_grow()
2625 if sh == 0.0 and gh != 0.0:
2626 sh = 15.*cm
2627 if sv == 0.0 and gv != 0.0:
2628 sv = 15.*cm*gv/gh * 1./golden_ratio
2629 self.set_size((sh, sv), (0., 0.))
2631 def get_children(self):
2633 '''
2634 Get sub-widgets contained in this widget.
2636 Returns a list of widgets.
2637 '''
2639 return []
2641 def get_size(self):
2643 '''
2644 Get current size and position of the widget.
2646 Triggers layouting and returns
2647 ``((width, height), (xoffset, yoffset))``
2648 '''
2650 self.do_layout()
2651 return (self.horizontal.get_value(),
2652 self.vertical.get_value()), self.offset
2654 def get_params(self):
2656 '''
2657 Get current size and position of the widget.
2659 Triggers layouting and returns dict with keys ``'xoffset'``,
2660 ``'yoffset'``, ``'width'`` and ``'height'``.
2661 '''
2663 self.do_layout()
2664 (w, h), (xo, yo) = self.get_size()
2665 return dict(xoffset=xo, yoffset=yo, width=w, height=h,
2666 width_m=w/_units['m'])
2668 def width(self):
2670 '''
2671 Get current width of the widget.
2673 Triggers layouting and returns width.
2674 '''
2676 self.do_layout()
2677 return self.horizontal.get_value()
2679 def height(self):
2681 '''
2682 Get current height of the widget.
2684 Triggers layouting and return height.
2685 '''
2687 self.do_layout()
2688 return self.vertical.get_value()
2690 def bbox(self):
2692 '''
2693 Get PostScript bounding box for this widget.
2695 Triggers layouting and returns values suitable to create PS bounding
2696 box, representing the widgets current size and position.
2697 '''
2699 self.do_layout()
2700 return (self.offset[0], self.offset[1], self.offset[0]+self.width(),
2701 self.offset[1]+self.height())
2703 def dirtyfy(self):
2705 '''
2706 Set dirty flag on top level widget in the hierarchy.
2708 Called by various methods, to indicate, that the widget hierarchy needs
2709 new layouting.
2710 '''
2712 if self.parent is not None:
2713 self.parent.dirtyfy()
2715 self.dirty = True
2718class CenterLayout(Widget):
2720 '''
2721 A layout manager which centers its single child widget.
2723 The child widget may be oversized.
2724 '''
2726 def __init__(self, horizontal=None, vertical=None):
2727 Widget.__init__(self, horizontal, vertical)
2728 self.content = Widget(horizontal=GumSpring(grow=1.),
2729 vertical=GumSpring(grow=1.), parent=self)
2731 def get_min_size(self):
2732 shs, svs = Widget.get_min_size(self)
2733 sh, sv = self.content.get_min_size()
2734 return max(shs, sh), max(svs, sv)
2736 def get_grow(self):
2737 ghs, gvs = Widget.get_grow(self)
2738 gh, gv = self.content.get_grow()
2739 return gh*ghs, gv*gvs
2741 def set_size(self, size, offset):
2742 (sh, sv), (oh, ov) = self.legalize(size, offset)
2744 shc, svc = self.content.get_min_size()
2745 ghc, gvc = self.content.get_grow()
2746 if ghc != 0.:
2747 shc = sh
2748 if gvc != 0.:
2749 svc = sv
2750 ohc = oh+(sh-shc)/2.
2751 ovc = ov+(sv-svc)/2.
2753 self.content.set_size((shc, svc), (ohc, ovc))
2754 Widget.set_size(self, (sh, sv), (oh, ov))
2756 def set_widget(self, widget=None):
2758 '''
2759 Set the child widget, which shall be centered.
2760 '''
2762 if widget is None:
2763 widget = Widget()
2765 self.content = widget
2767 widget.set_parent(self)
2769 def get_widget(self):
2770 return self.content
2772 def get_children(self):
2773 return [self.content]
2776class FrameLayout(Widget):
2778 '''
2779 A layout manager containing a center widget sorrounded by four margin
2780 widgets.
2782 ::
2784 +---------------------------+
2785 | top |
2786 +---------------------------+
2787 | | | |
2788 | left | center | right |
2789 | | | |
2790 +---------------------------+
2791 | bottom |
2792 +---------------------------+
2794 This layout manager does a little bit of extra effort to maintain the
2795 aspect constraint of the center widget, if this is set. It does so, by
2796 allowing for a bit more flexibility in the sizing of the margins. Two
2797 shortcut methods are provided to set the margin sizes in one shot:
2798 :py:meth:`set_fixed_margins` and :py:meth:`set_min_margins`. The first sets
2799 the margins to fixed sizes, while the second gives them a minimal size and
2800 a (neglectably) small desire to grow. Using the latter may be useful when
2801 setting an aspect constraint on the center widget, because this way the
2802 maximum size of the center widget may be controlled without creating empty
2803 spaces between the widgets.
2804 '''
2806 def __init__(self, horizontal=None, vertical=None):
2807 Widget.__init__(self, horizontal, vertical)
2808 mw = 3.*cm
2809 self.left = Widget(
2810 horizontal=GumSpring(grow=0.15, minimal=mw), parent=self)
2811 self.right = Widget(
2812 horizontal=GumSpring(grow=0.15, minimal=mw), parent=self)
2813 self.top = Widget(
2814 vertical=GumSpring(grow=0.15, minimal=mw/golden_ratio),
2815 parent=self)
2816 self.bottom = Widget(
2817 vertical=GumSpring(grow=0.15, minimal=mw/golden_ratio),
2818 parent=self)
2819 self.center = Widget(
2820 horizontal=GumSpring(grow=0.7), vertical=GumSpring(grow=0.7),
2821 parent=self)
2823 def set_fixed_margins(self, left, right, top, bottom):
2824 '''
2825 Give margins fixed size constraints.
2826 '''
2828 self.left.set_horizontal(left, 0)
2829 self.right.set_horizontal(right, 0)
2830 self.top.set_vertical(top, 0)
2831 self.bottom.set_vertical(bottom, 0)
2833 def set_min_margins(self, left, right, top, bottom, grow=0.0001):
2834 '''
2835 Give margins a minimal size and the possibility to grow.
2837 The desire to grow is set to a very small number.
2838 '''
2839 self.left.set_horizontal(left, grow)
2840 self.right.set_horizontal(right, grow)
2841 self.top.set_vertical(top, grow)
2842 self.bottom.set_vertical(bottom, grow)
2844 def get_min_size(self):
2845 shs, svs = Widget.get_min_size(self)
2847 sl, sr, st, sb, sc = [x.get_min_size() for x in (
2848 self.left, self.right, self.top, self.bottom, self.center)]
2849 gl, gr, gt, gb, gc = [x.get_grow() for x in (
2850 self.left, self.right, self.top, self.bottom, self.center)]
2852 shsum = sl[0]+sr[0]+sc[0]
2853 svsum = st[1]+sb[1]+sc[1]
2855 # prevent widgets from collapsing
2856 for s, g in ((sl, gl), (sr, gr), (sc, gc)):
2857 if s[0] == 0.0 and g[0] != 0.0:
2858 shsum += 0.1*cm
2860 for s, g in ((st, gt), (sb, gb), (sc, gc)):
2861 if s[1] == 0.0 and g[1] != 0.0:
2862 svsum += 0.1*cm
2864 sh = max(shs, shsum)
2865 sv = max(svs, svsum)
2867 return sh, sv
2869 def get_grow(self):
2870 ghs, gvs = Widget.get_grow(self)
2871 gh = (self.left.get_grow()[0] +
2872 self.right.get_grow()[0] +
2873 self.center.get_grow()[0]) * ghs
2874 gv = (self.top.get_grow()[1] +
2875 self.bottom.get_grow()[1] +
2876 self.center.get_grow()[1]) * gvs
2877 return gh, gv
2879 def set_size(self, size, offset):
2880 (sh, sv), (oh, ov) = self.legalize(size, offset)
2882 sl, sr, st, sb, sc = [x.get_min_size() for x in (
2883 self.left, self.right, self.top, self.bottom, self.center)]
2884 gl, gr, gt, gb, gc = [x.get_grow() for x in (
2885 self.left, self.right, self.top, self.bottom, self.center)]
2887 ah = sh - (sl[0]+sr[0]+sc[0])
2888 av = sv - (st[1]+sb[1]+sc[1])
2890 if ah < 0.0:
2891 raise GmtPyError("Container not wide enough for contents "
2892 "(FrameLayout, available: %g cm, needed: %g cm)"
2893 % (sh/cm, (sl[0]+sr[0]+sc[0])/cm))
2894 if av < 0.0:
2895 raise GmtPyError("Container not high enough for contents "
2896 "(FrameLayout, available: %g cm, needed: %g cm)"
2897 % (sv/cm, (st[1]+sb[1]+sc[1])/cm))
2899 slh, srh, sch = distribute((sl[0], sr[0], sc[0]),
2900 (gl[0], gr[0], gc[0]), ah)
2901 stv, sbv, scv = distribute((st[1], sb[1], sc[1]),
2902 (gt[1], gb[1], gc[1]), av)
2904 if self.center.aspect is not None:
2905 ahm = sh - (sl[0]+sr[0] + scv/self.center.aspect)
2906 avm = sv - (st[1]+sb[1] + sch*self.center.aspect)
2907 if 0.0 < ahm < ah:
2908 slh, srh, sch = distribute(
2909 (sl[0], sr[0], scv/self.center.aspect),
2910 (gl[0], gr[0], 0.0), ahm)
2912 elif 0.0 < avm < av:
2913 stv, sbv, scv = distribute((st[1], sb[1],
2914 sch*self.center.aspect),
2915 (gt[1], gb[1], 0.0), avm)
2917 ah = sh - (slh+srh+sch)
2918 av = sv - (stv+sbv+scv)
2920 oh += ah/2.
2921 ov += av/2.
2922 sh -= ah
2923 sv -= av
2925 self.left.set_size((slh, scv), (oh, ov+sbv))
2926 self.right.set_size((srh, scv), (oh+slh+sch, ov+sbv))
2927 self.top.set_size((sh, stv), (oh, ov+sbv+scv))
2928 self.bottom.set_size((sh, sbv), (oh, ov))
2929 self.center.set_size((sch, scv), (oh+slh, ov+sbv))
2930 Widget.set_size(self, (sh, sv), (oh, ov))
2932 def set_widget(self, which='center', widget=None):
2934 '''
2935 Set one of the sub-widgets.
2937 ``which`` should be one of ``'left'``, ``'right'``, ``'top'``,
2938 ``'bottom'`` or ``'center'``.
2939 '''
2941 if widget is None:
2942 widget = Widget()
2944 if which in ('left', 'right', 'top', 'bottom', 'center'):
2945 self.__dict__[which] = widget
2946 else:
2947 raise GmtPyError('No such sub-widget: %s' % which)
2949 widget.set_parent(self)
2951 def get_widget(self, which='center'):
2953 '''
2954 Get one of the sub-widgets.
2956 ``which`` should be one of ``'left'``, ``'right'``, ``'top'``,
2957 ``'bottom'`` or ``'center'``.
2958 '''
2960 if which in ('left', 'right', 'top', 'bottom', 'center'):
2961 return self.__dict__[which]
2962 else:
2963 raise GmtPyError('No such sub-widget: %s' % which)
2965 def get_children(self):
2966 return [self.left, self.right, self.top, self.bottom, self.center]
2969class GridLayout(Widget):
2971 '''
2972 A layout manager which arranges its sub-widgets in a grid.
2974 The grid spacing is flexible and based on the sizing policies of the
2975 contained sub-widgets. If an equidistant grid is needed, the sizing
2976 policies of the sub-widgets have to be set equally.
2978 The height of each row and the width of each column is derived from the
2979 sizing policy of the largest sub-widget in the row or column in question.
2980 The algorithm is not very sophisticated, so conflicting sizing policies
2981 might not be resolved optimally.
2982 '''
2984 def __init__(self, nx=2, ny=2, horizontal=None, vertical=None):
2986 '''
2987 Create new grid layout with ``nx`` columns and ``ny`` rows.
2988 '''
2990 Widget.__init__(self, horizontal, vertical)
2991 self.grid = []
2992 for iy in range(ny):
2993 row = []
2994 for ix in range(nx):
2995 w = Widget(parent=self)
2996 row.append(w)
2998 self.grid.append(row)
3000 def sub_min_sizes_as_array(self):
3001 esh = num.array(
3002 [[w.get_min_size()[0] for w in row] for row in self.grid],
3003 dtype=float)
3004 esv = num.array(
3005 [[w.get_min_size()[1] for w in row] for row in self.grid],
3006 dtype=float)
3007 return esh, esv
3009 def sub_grows_as_array(self):
3010 egh = num.array(
3011 [[w.get_grow()[0] for w in row] for row in self.grid],
3012 dtype=float)
3013 egv = num.array(
3014 [[w.get_grow()[1] for w in row] for row in self.grid],
3015 dtype=float)
3016 return egh, egv
3018 def get_min_size(self):
3019 sh, sv = Widget.get_min_size(self)
3020 esh, esv = self.sub_min_sizes_as_array()
3021 if esh.size != 0:
3022 sh = max(sh, num.sum(esh.max(0)))
3023 if esv.size != 0:
3024 sv = max(sv, num.sum(esv.max(1)))
3025 return sh, sv
3027 def get_grow(self):
3028 ghs, gvs = Widget.get_grow(self)
3029 egh, egv = self.sub_grows_as_array()
3030 if egh.size != 0:
3031 gh = num.sum(egh.max(0))*ghs
3032 else:
3033 gh = 1.0
3034 if egv.size != 0:
3035 gv = num.sum(egv.max(1))*gvs
3036 else:
3037 gv = 1.0
3038 return gh, gv
3040 def set_size(self, size, offset):
3041 (sh, sv), (oh, ov) = self.legalize(size, offset)
3042 esh, esv = self.sub_min_sizes_as_array()
3043 egh, egv = self.sub_grows_as_array()
3045 # available additional space
3046 empty = esh.size == 0
3048 if not empty:
3049 ah = sh - num.sum(esh.max(0))
3050 av = sv - num.sum(esv.max(1))
3051 else:
3052 av = sv
3053 ah = sh
3055 if ah < 0.0:
3056 raise GmtPyError("Container not wide enough for contents "
3057 "(GridLayout, available: %g cm, needed: %g cm)"
3058 % (sh/cm, (num.sum(esh.max(0)))/cm))
3059 if av < 0.0:
3060 raise GmtPyError("Container not high enough for contents "
3061 "(GridLayout, available: %g cm, needed: %g cm)"
3062 % (sv/cm, (num.sum(esv.max(1)))/cm))
3064 nx, ny = esh.shape
3066 if not empty:
3067 # distribute additional space on rows and columns
3068 # according to grow weights and minimal sizes
3069 gsh = egh.sum(1)[:, num.newaxis].repeat(ny, axis=1)
3070 nesh = esh.copy()
3071 nesh += num.where(gsh > 0.0, ah*egh/gsh, 0.0)
3073 nsh = num.maximum(nesh.max(0), esh.max(0))
3075 gsv = egv.sum(0)[num.newaxis, :].repeat(nx, axis=0)
3076 nesv = esv.copy()
3077 nesv += num.where(gsv > 0.0, av*egv/gsv, 0.0)
3078 nsv = num.maximum(nesv.max(1), esv.max(1))
3080 ah = sh - sum(nsh)
3081 av = sv - sum(nsv)
3083 oh += ah/2.
3084 ov += av/2.
3085 sh -= ah
3086 sv -= av
3088 # resize child widgets
3089 neov = ov + sum(nsv)
3090 for row, nesv in zip(self.grid, nsv):
3091 neov -= nesv
3092 neoh = oh
3093 for w, nesh in zip(row, nsh):
3094 w.set_size((nesh, nesv), (neoh, neov))
3095 neoh += nesh
3097 Widget.set_size(self, (sh, sv), (oh, ov))
3099 def set_widget(self, ix, iy, widget=None):
3101 '''
3102 Set one of the sub-widgets.
3104 Sets the sub-widget in column ``ix`` and row ``iy``. The indices are
3105 counted from zero.
3106 '''
3108 if widget is None:
3109 widget = Widget()
3111 self.grid[iy][ix] = widget
3112 widget.set_parent(self)
3114 def get_widget(self, ix, iy):
3116 '''
3117 Get one of the sub-widgets.
3119 Gets the sub-widget from column ``ix`` and row ``iy``. The indices are
3120 counted from zero.
3121 '''
3123 return self.grid[iy][ix]
3125 def get_children(self):
3126 children = []
3127 for row in self.grid:
3128 children.extend(row)
3130 return children
3133def is_gmt5(version='newest'):
3134 return get_gmt_installation(version)['version'][0] in ['5', '6']
3137def is_gmt6(version='newest'):
3138 return get_gmt_installation(version)['version'][0] in ['6']
3141def aspect_for_projection(gmtversion, *args, **kwargs):
3143 gmt = GMT(version=gmtversion, eps_mode=True)
3145 if gmt.is_gmt5():
3146 gmt.psbasemap('-B+gblack', finish=True, *args, **kwargs)
3147 fn = gmt.tempfilename('test.eps')
3148 gmt.save(fn, crop_eps_mode=True)
3149 with open(fn, 'rb') as f:
3150 s = f.read()
3152 l, b, r, t = get_bbox(s)
3153 else:
3154 gmt.psbasemap('-G0', finish=True, *args, **kwargs)
3155 l, b, r, t = gmt.bbox()
3157 return (t-b)/(r-l)
3160def text_box(
3161 text, font=0, font_size=12., angle=0, gmtversion='newest', **kwargs):
3163 gmt = GMT(version=gmtversion)
3164 if gmt.is_gmt5():
3165 row = [0, 0, text]
3166 farg = ['-F+f%gp,%s,%s+j%s' % (font_size, font, 'black', 'BL')]
3167 else:
3168 row = [0, 0, font_size, 0, font, 'BL', text]
3169 farg = []
3171 gmt.pstext(
3172 in_rows=[row],
3173 finish=True,
3174 R=(0, 1, 0, 1),
3175 J='x10p',
3176 N=True,
3177 *farg,
3178 **kwargs)
3180 fn = gmt.tempfilename() + '.ps'
3181 gmt.save(fn)
3183 (_, stderr) = subprocess.Popen(
3184 ['gs', '-q', '-dNOPAUSE', '-dBATCH', '-r720', '-sDEVICE=bbox', fn],
3185 stderr=subprocess.PIPE).communicate()
3187 dx, dy = None, None
3188 for line in stderr.splitlines():
3189 if line.startswith(b'%%HiResBoundingBox:'):
3190 l, b, r, t = [float(x) for x in line.split()[-4:]]
3191 dx, dy = r-l, t-b
3192 break
3194 return dx, dy
3197class TableLiner(object):
3198 '''
3199 Utility class to turn tables into lines.
3200 '''
3202 def __init__(self, in_columns=None, in_rows=None, encoding='utf-8'):
3203 self.in_columns = in_columns
3204 self.in_rows = in_rows
3205 self.encoding = encoding
3207 def __iter__(self):
3208 if self.in_columns is not None:
3209 for row in zip(*self.in_columns):
3210 yield (' '.join([str(x) for x in row])+'\n').encode(
3211 self.encoding)
3213 if self.in_rows is not None:
3214 for row in self.in_rows:
3215 yield (' '.join([str(x) for x in row])+'\n').encode(
3216 self.encoding)
3219class LineStreamChopper(object):
3220 '''
3221 File-like object to buffer data.
3222 '''
3224 def __init__(self, liner):
3225 self.chopsize = None
3226 self.liner = liner
3227 self.chop_iterator = None
3228 self.closed = False
3230 def _chopiter(self):
3231 buf = BytesIO()
3232 for line in self.liner:
3233 buf.write(line)
3234 buflen = buf.tell()
3235 if self.chopsize is not None and buflen >= self.chopsize:
3236 buf.seek(0)
3237 while buf.tell() <= buflen-self.chopsize:
3238 yield buf.read(self.chopsize)
3240 newbuf = BytesIO()
3241 newbuf.write(buf.read())
3242 buf.close()
3243 buf = newbuf
3245 yield buf.getvalue()
3246 buf.close()
3248 def read(self, size=None):
3249 if self.closed:
3250 raise ValueError('Cannot read from closed LineStreamChopper.')
3251 if self.chop_iterator is None:
3252 self.chopsize = size
3253 self.chop_iterator = self._chopiter()
3255 self.chopsize = size
3256 try:
3257 return next(self.chop_iterator)
3258 except StopIteration:
3259 return ''
3261 def close(self):
3262 self.chopsize = None
3263 self.chop_iterator = None
3264 self.closed = True
3266 def flush(self):
3267 pass
3270font_tab = {
3271 0: 'Helvetica',
3272 1: 'Helvetica-Bold',
3273}
3275font_tab_rev = dict((v, k) for (k, v) in font_tab.items())
3278class GMT(object):
3279 '''
3280 A thin wrapper to GMT command execution.
3282 A dict ``config`` may be given to override some of the default GMT
3283 parameters. The ``version`` argument may be used to select a specific GMT
3284 version, which should be used with this GMT instance. The selected
3285 version of GMT has to be installed on the system, must be supported by
3286 gmtpy and gmtpy must know where to find it.
3288 Each instance of this class is used for the task of producing one PS or PDF
3289 output file.
3291 Output of a series of GMT commands is accumulated in memory and can then be
3292 saved as PS or PDF file using the :py:meth:`save` method.
3294 GMT commands are accessed as method calls to instances of this class. See
3295 the :py:meth:`__getattr__` method for details on how the method's
3296 arguments are translated into options and arguments for the GMT command.
3298 Associated with each instance of this class, a temporary directory is
3299 created, where temporary files may be created, and which is automatically
3300 deleted, when the object is destroyed. The :py:meth:`tempfilename` method
3301 may be used to get a random filename in the instance's temporary directory.
3303 Any .gmtdefaults files are ignored. The GMT class uses a fixed
3304 set of defaults, which may be altered via an argument to the constructor.
3305 If possible, GMT is run in 'isolation mode', which was introduced with GMT
3306 version 4.2.2, by setting `GMT_TMPDIR` to the instance's temporary
3307 directory. With earlier versions of GMT, problems may arise with parallel
3308 execution of more than one GMT instance.
3310 Each instance of the GMT class may pick a specific version of GMT which
3311 shall be used, so that, if multiple versions of GMT are installed on the
3312 system, different versions of GMT can be used simultaneously such that
3313 backward compatibility of the scripts can be maintained.
3315 '''
3317 def __init__(
3318 self,
3319 config=None,
3320 kontinue=None,
3321 version='newest',
3322 config_papersize=None,
3323 eps_mode=False):
3325 self.installation = get_gmt_installation(version)
3326 self.gmt_config = dict(self.installation['defaults'])
3327 self.eps_mode = eps_mode
3328 self._shutil = shutil
3330 if config:
3331 self.gmt_config.update(config)
3333 if config_papersize:
3334 if not isinstance(config_papersize, str):
3335 config_papersize = 'Custom_%ix%i' % (
3336 int(config_papersize[0]), int(config_papersize[1]))
3338 if self.is_gmt5():
3339 self.gmt_config['PS_MEDIA'] = config_papersize
3340 else:
3341 self.gmt_config['PAPER_MEDIA'] = config_papersize
3343 self.tempdir = tempfile.mkdtemp("", "gmtpy-")
3344 self.gmt_config_filename = pjoin(self.tempdir, 'gmt.conf')
3345 self.gen_gmt_config_file(self.gmt_config_filename, self.gmt_config)
3347 if kontinue is not None:
3348 self.load_unfinished(kontinue)
3349 self.needstart = False
3350 else:
3351 self.output = BytesIO()
3352 self.needstart = True
3354 self.finished = False
3356 self.environ = os.environ.copy()
3357 self.environ['GMTHOME'] = self.installation.get('home', '')
3358 # GMT isolation mode: works only properly with GMT version >= 4.2.2
3359 self.environ['GMT_TMPDIR'] = self.tempdir
3361 self.layout = None
3362 self.command_log = []
3363 self.keep_temp_dir = False
3365 def is_gmt5(self):
3366 return self.get_version()[0] in ['5', '6']
3368 def is_gmt6(self):
3369 return self.get_version()[0] in ['6']
3371 def get_version(self):
3372 return self.installation['version']
3374 def get_config(self, key):
3375 return self.gmt_config[key]
3377 def to_points(self, string):
3378 if not string:
3379 return 0
3381 unit = string[-1]
3382 if unit in _units:
3383 return float(string[:-1])/_units[unit]
3384 else:
3385 default_unit = measure_unit(self.gmt_config).lower()[0]
3386 return float(string)/_units[default_unit]
3388 def label_font_size(self):
3389 if self.is_gmt5():
3390 return self.to_points(self.gmt_config['FONT_LABEL'].split(',')[0])
3391 else:
3392 return self.to_points(self.gmt_config['LABEL_FONT_SIZE'])
3394 def label_font(self):
3395 if self.is_gmt5():
3396 return font_tab_rev(self.gmt_config['FONT_LABEL'].split(',')[1])
3397 else:
3398 return self.gmt_config['LABEL_FONT']
3400 def gen_gmt_config_file(self, config_filename, config):
3401 f = open(config_filename, 'wb')
3402 f.write(
3403 ('#\n# GMT %s Defaults file\n'
3404 % self.installation['version']).encode('ascii'))
3406 for k, v in config.items():
3407 f.write(('%s = %s\n' % (k, v)).encode('ascii'))
3408 f.close()
3410 def __del__(self):
3411 if not self.keep_temp_dir:
3412 self._shutil.rmtree(self.tempdir)
3414 def _gmtcommand(self, command, *addargs, **kwargs):
3416 '''
3417 Execute arbitrary GMT command.
3419 See docstring in __getattr__ for details.
3420 '''
3422 in_stream = kwargs.pop('in_stream', None)
3423 in_filename = kwargs.pop('in_filename', None)
3424 in_string = kwargs.pop('in_string', None)
3425 in_columns = kwargs.pop('in_columns', None)
3426 in_rows = kwargs.pop('in_rows', None)
3427 out_stream = kwargs.pop('out_stream', None)
3428 out_filename = kwargs.pop('out_filename', None)
3429 out_discard = kwargs.pop('out_discard', None)
3430 finish = kwargs.pop('finish', False)
3431 suppressdefaults = kwargs.pop('suppress_defaults', False)
3432 config_override = kwargs.pop('config', None)
3434 assert not self.finished
3436 # check for mutual exclusiveness on input and output possibilities
3437 assert (1 >= len(
3438 [x for x in [
3439 in_stream, in_filename, in_string, in_columns, in_rows]
3440 if x is not None]))
3441 assert (1 >= len([x for x in [out_stream, out_filename, out_discard]
3442 if x is not None]))
3444 options = []
3446 gmt_config = self.gmt_config
3447 if not self.is_gmt5():
3448 gmt_config_filename = self.gmt_config_filename
3449 if config_override:
3450 gmt_config = self.gmt_config.copy()
3451 gmt_config.update(config_override)
3452 gmt_config_override_filename = pjoin(
3453 self.tempdir, 'gmtdefaults_override')
3454 self.gen_gmt_config_file(
3455 gmt_config_override_filename, gmt_config)
3456 gmt_config_filename = gmt_config_override_filename
3458 else: # gmt5 needs override variables as --VAR=value
3459 if config_override:
3460 for k, v in config_override.items():
3461 options.append('--%s=%s' % (k, v))
3463 if out_discard:
3464 out_filename = '/dev/null'
3466 out_mustclose = False
3467 if out_filename is not None:
3468 out_mustclose = True
3469 out_stream = open(out_filename, 'wb')
3471 if in_filename is not None:
3472 in_stream = open(in_filename, 'rb')
3474 if in_string is not None:
3475 in_stream = BytesIO(in_string)
3477 encoding_gmt = gmt_config.get(
3478 'PS_CHAR_ENCODING',
3479 gmt_config.get('CHAR_ENCODING', 'ISOLatin1+'))
3481 encoding = encoding_gmt_to_python[encoding_gmt.lower()]
3483 if in_columns is not None or in_rows is not None:
3484 in_stream = LineStreamChopper(TableLiner(in_columns=in_columns,
3485 in_rows=in_rows,
3486 encoding=encoding))
3488 # convert option arguments to strings
3489 for k, v in kwargs.items():
3490 if len(k) > 1:
3491 raise GmtPyError('Found illegal keyword argument "%s" '
3492 'while preparing options for command "%s"'
3493 % (k, command))
3495 if type(v) is bool:
3496 if v:
3497 options.append('-%s' % k)
3498 elif type(v) is tuple or type(v) is list:
3499 options.append('-%s' % k + '/'.join([str(x) for x in v]))
3500 else:
3501 options.append('-%s%s' % (k, str(v)))
3503 # if not redirecting to an external sink, handle -K -O
3504 if out_stream is None:
3505 if not finish:
3506 options.append('-K')
3507 else:
3508 self.finished = True
3510 if not self.needstart:
3511 options.append('-O')
3512 else:
3513 self.needstart = False
3515 out_stream = self.output
3517 # run the command
3518 if self.is_gmt5():
3519 args = [pjoin(self.installation['bin'], 'gmt'), command]
3520 else:
3521 args = [pjoin(self.installation['bin'], command)]
3523 if not os.path.isfile(args[0]):
3524 raise OSError('No such file: %s' % args[0])
3525 args.extend(options)
3526 args.extend(addargs)
3527 if not self.is_gmt5() and not suppressdefaults:
3528 # does not seem to work with GMT 5 (and should not be necessary
3529 args.append('+'+gmt_config_filename)
3531 bs = 2048
3532 p = subprocess.Popen(args, stdin=subprocess.PIPE,
3533 stdout=subprocess.PIPE, bufsize=bs,
3534 env=self.environ)
3535 while True:
3536 cr, cw, cx = select([p.stdout], [p.stdin], [])
3537 if cr:
3538 out_stream.write(p.stdout.read(bs))
3539 if cw:
3540 if in_stream is not None:
3541 data = in_stream.read(bs)
3542 if len(data) == 0:
3543 break
3544 p.stdin.write(data)
3545 else:
3546 break
3547 if not cr and not cw:
3548 break
3550 p.stdin.close()
3552 while True:
3553 data = p.stdout.read(bs)
3554 if len(data) == 0:
3555 break
3556 out_stream.write(data)
3558 p.stdout.close()
3560 retcode = p.wait()
3562 if in_stream is not None:
3563 in_stream.close()
3565 if out_mustclose:
3566 out_stream.close()
3568 if retcode != 0:
3569 self.keep_temp_dir = True
3570 raise GMTError('Command %s returned an error. '
3571 'While executing command:\n%s'
3572 % (command, escape_shell_args(args)))
3574 self.command_log.append(args)
3576 def __getattr__(self, command):
3578 '''
3579 Maps to call self._gmtcommand(command, \\*addargs, \\*\\*kwargs).
3581 Execute arbitrary GMT command.
3583 Run a GMT command and by default append its postscript output to the
3584 output file maintained by the GMT instance on which this method is
3585 called.
3587 Except for a few keyword arguments listed below, any ``kwargs`` and
3588 ``addargs`` are converted into command line options and arguments and
3589 passed to the GMT command. Numbers in keyword arguments are converted
3590 into strings. E.g. ``S=10`` is translated into ``'-S10'``. Tuples of
3591 numbers or strings are converted into strings where the elements of the
3592 tuples are separated by slashes '/'. E.g. ``R=(10, 10, 20, 20)`` is
3593 translated into ``'-R10/10/20/20'``. Options with a boolean argument
3594 are only appended to the GMT command, if their values are True.
3596 If no output redirection is in effect, the -K and -O options are
3597 handled by gmtpy and thus should not be specified. Use
3598 ``out_discard=True`` if you don't want -K or -O beeing added, but are
3599 not interested in the output.
3601 The standard input of the GMT process is fed by data selected with one
3602 of the following ``in_*`` keyword arguments:
3604 =============== =======================================================
3605 ``in_stream`` Data is read from an open file like object.
3606 ``in_filename`` Data is read from the given file.
3607 ``in_string`` String content is dumped to the process.
3608 ``in_columns`` A 2D nested iterable whose elements can be accessed as
3609 ``in_columns[icolumn][irow]`` is converted into an
3610 ascii
3611 table, which is fed to the process.
3612 ``in_rows`` A 2D nested iterable whos elements can be accessed as
3613 ``in_rows[irow][icolumn]`` is converted into an ascii
3614 table, which is fed to the process.
3615 =============== =======================================================
3617 The standard output of the GMT process may be redirected by one of the
3618 following options:
3620 ================= =====================================================
3621 ``out_stream`` Output is fed to an open file like object.
3622 ``out_filename`` Output is dumped to the given file.
3623 ``out_discard`` If True, output is dumped to :file:`/dev/null`.
3624 ================= =====================================================
3626 Additional keyword arguments:
3628 ===================== =================================================
3629 ``config`` Dict with GMT defaults which override the
3630 currently active set of defaults exclusively
3631 during this call.
3632 ``finish`` If True, the postscript file, which is maintained
3633 by the GMT instance is finished, and no further
3634 plotting is allowed.
3635 ``suppress_defaults`` Suppress appending of the ``'+gmtdefaults'``
3636 option to the command.
3637 ===================== =================================================
3639 '''
3641 def f(*args, **kwargs):
3642 return self._gmtcommand(command, *args, **kwargs)
3643 return f
3645 def tempfilename(self, name=None):
3646 '''
3647 Get filename for temporary file in the private temp directory.
3649 If no ``name`` argument is given, a random name is picked. If
3650 ``name`` is given, returns a path ending in that ``name``.
3651 '''
3653 if not name:
3654 name = ''.join(
3655 [random.choice('abcdefghijklmnopqrstuvwxyz')
3656 for i in range(10)])
3658 fn = pjoin(self.tempdir, name)
3659 return fn
3661 def tempfile(self, name=None):
3662 '''
3663 Create and open a file in the private temp directory.
3664 '''
3666 fn = self.tempfilename(name)
3667 f = open(fn, 'wb')
3668 return f, fn
3670 def save_unfinished(self, filename):
3671 out = open(filename, 'wb')
3672 out.write(self.output.getvalue())
3673 out.close()
3675 def load_unfinished(self, filename):
3676 self.output = BytesIO()
3677 self.finished = False
3678 inp = open(filename, 'rb')
3679 self.output.write(inp.read())
3680 inp.close()
3682 def dump(self, ident):
3683 filename = self.tempfilename('breakpoint-%s' % ident)
3684 self.save_unfinished(filename)
3686 def load(self, ident):
3687 filename = self.tempfilename('breakpoint-%s' % ident)
3688 self.load_unfinished(filename)
3690 def save(self, filename=None, bbox=None, resolution=150, oversample=2.,
3691 width=None, height=None, size=None, crop_eps_mode=False,
3692 psconvert=False):
3694 '''
3695 Finish and save figure as PDF, PS or PPM file.
3697 If filename ends with ``'.pdf'`` a PDF file is created by piping the
3698 GMT output through :program:`gmtpy-epstopdf`.
3700 If filename ends with ``'.png'`` a PNG file is created by running
3701 :program:`gmtpy-epstopdf`, :program:`pdftocairo` and
3702 :program:`convert`. ``resolution`` specifies the resolution in DPI for
3703 raster file formats. Rasterization is done at a higher resolution if
3704 ``oversample`` is set to a value higher than one. The output image size
3705 can also be controlled by setting ``width``, ``height`` or ``size``
3706 instead of ``resolution``. When ``size`` is given, the image is scaled
3707 so that ``max(width, height) == size``.
3709 The bounding box is set according to the values given in ``bbox``.
3710 '''
3712 if not self.finished:
3713 self.psxy(R=True, J=True, finish=True)
3715 if filename:
3716 tempfn = pjoin(self.tempdir, 'incomplete')
3717 out = open(tempfn, 'wb')
3718 else:
3719 out = sys.stdout
3721 if bbox and not self.is_gmt5():
3722 out.write(replace_bbox(bbox, self.output.getvalue()))
3723 else:
3724 out.write(self.output.getvalue())
3726 if filename:
3727 out.close()
3729 if filename.endswith('.ps') or (
3730 not self.is_gmt5() and filename.endswith('.eps')):
3732 shutil.move(tempfn, filename)
3733 return
3735 if self.is_gmt5():
3736 if crop_eps_mode:
3737 addarg = ['-A']
3738 else:
3739 addarg = []
3741 subprocess.call(
3742 [pjoin(self.installation['bin'], 'gmt'), 'psconvert',
3743 '-Te', '-F%s' % tempfn, tempfn, ] + addarg)
3745 if bbox:
3746 with open(tempfn + '.eps', 'rb') as fin:
3747 with open(tempfn + '-fixbb.eps', 'wb') as fout:
3748 replace_bbox(bbox, fin, fout)
3750 shutil.move(tempfn + '-fixbb.eps', tempfn + '.eps')
3752 else:
3753 shutil.move(tempfn, tempfn + '.eps')
3755 if filename.endswith('.eps'):
3756 shutil.move(tempfn + '.eps', filename)
3757 return
3759 elif filename.endswith('.pdf'):
3760 if psconvert:
3761 gmt_bin = pjoin(self.installation['bin'], 'gmt')
3762 subprocess.call([gmt_bin, 'psconvert', tempfn + '.eps', '-Tf',
3763 '-F' + filename])
3764 else:
3765 subprocess.call(['gmtpy-epstopdf', '--res=%i' % resolution,
3766 '--outfile=' + filename, tempfn + '.eps'])
3767 else:
3768 subprocess.call([
3769 'gmtpy-epstopdf',
3770 '--res=%i' % (resolution * oversample),
3771 '--outfile=' + tempfn + '.pdf', tempfn + '.eps'])
3773 convert_graph(
3774 tempfn + '.pdf', filename,
3775 resolution=resolution, oversample=oversample,
3776 size=size, width=width, height=height)
3778 def bbox(self):
3779 return get_bbox(self.output.getvalue())
3781 def get_command_log(self):
3782 '''
3783 Get the command log.
3784 '''
3786 return self.command_log
3788 def __str__(self):
3789 s = ''
3790 for com in self.command_log:
3791 s += com[0] + "\n " + "\n ".join(com[1:]) + "\n\n"
3792 return s
3794 def page_size_points(self):
3795 '''
3796 Try to get paper size of output postscript file in points.
3797 '''
3799 pm = paper_media(self.gmt_config).lower()
3800 if pm.endswith('+') or pm.endswith('-'):
3801 pm = pm[:-1]
3803 orient = page_orientation(self.gmt_config).lower()
3805 if pm in all_paper_sizes():
3807 if orient == 'portrait':
3808 return get_paper_size(pm)
3809 else:
3810 return get_paper_size(pm)[1], get_paper_size(pm)[0]
3812 m = re.match(r'custom_([0-9.]+)([cimp]?)x([0-9.]+)([cimp]?)', pm)
3813 if m:
3814 w, uw, h, uh = m.groups()
3815 w, h = float(w), float(h)
3816 if uw:
3817 w *= _units[uw]
3818 if uh:
3819 h *= _units[uh]
3820 if orient == 'portrait':
3821 return w, h
3822 else:
3823 return h, w
3825 return None, None
3827 def default_layout(self, with_palette=False):
3828 '''
3829 Get a default layout for the output page.
3831 One of three different layouts is choosen, depending on the
3832 `PAPER_MEDIA` setting in the GMT configuration dict.
3834 If `PAPER_MEDIA` ends with a ``'+'`` (EPS output is selected), a
3835 :py:class:`FrameLayout` is centered on the page, whose size is
3836 controlled by its center widget's size plus the margins of the
3837 :py:class:`FrameLayout`.
3839 If `PAPER_MEDIA` indicates, that a custom page size is wanted by
3840 starting with ``'Custom_'``, a :py:class:`FrameLayout` is used to fill
3841 the complete page. The center widget's size is then controlled by the
3842 page's size minus the margins of the :py:class:`FrameLayout`.
3844 In any other case, two FrameLayouts are nested, such that the outer
3845 layout attaches a 1 cm (printer) margin around the complete page, and
3846 the inner FrameLayout's center widget takes up as much space as
3847 possible under the constraint, that an aspect ratio of 1/golden_ratio
3848 is preserved.
3850 In any case, a reference to the innermost :py:class:`FrameLayout`
3851 instance is returned. The top-level layout can be accessed by calling
3852 :py:meth:`Widget.get_parent` on the returned layout.
3853 '''
3855 if self.layout is None:
3856 w, h = self.page_size_points()
3858 if w is None or h is None:
3859 raise GmtPyError("Can't determine page size for layout")
3861 pm = paper_media(self.gmt_config).lower()
3863 if with_palette:
3864 palette_layout = GridLayout(3, 1)
3865 spacer = palette_layout.get_widget(1, 0)
3866 palette_widget = palette_layout.get_widget(2, 0)
3867 spacer.set_horizontal(0.5*cm)
3868 palette_widget.set_horizontal(0.5*cm)
3870 if pm.endswith('+') or self.eps_mode:
3871 outer = CenterLayout()
3872 outer.set_policy((w, h), (0., 0.))
3873 inner = FrameLayout()
3874 outer.set_widget(inner)
3875 if with_palette:
3876 inner.set_widget('center', palette_layout)
3877 widget = palette_layout
3878 else:
3879 widget = inner.get_widget('center')
3880 widget.set_policy((w/golden_ratio, 0.), (0., 0.),
3881 aspect=1./golden_ratio)
3882 mw = 3.0*cm
3883 inner.set_fixed_margins(
3884 mw, mw, mw/golden_ratio, mw/golden_ratio)
3885 self.layout = inner
3887 elif pm.startswith('custom_'):
3888 layout = FrameLayout()
3889 layout.set_policy((w, h), (0., 0.))
3890 mw = 3.0*cm
3891 layout.set_min_margins(
3892 mw, mw, mw/golden_ratio, mw/golden_ratio)
3893 if with_palette:
3894 layout.set_widget('center', palette_layout)
3895 self.layout = layout
3896 else:
3897 outer = FrameLayout()
3898 outer.set_policy((w, h), (0., 0.))
3899 outer.set_fixed_margins(1.*cm, 1.*cm, 1.*cm, 1.*cm)
3901 inner = FrameLayout()
3902 outer.set_widget('center', inner)
3903 mw = 3.0*cm
3904 inner.set_min_margins(mw, mw, mw/golden_ratio, mw/golden_ratio)
3905 if with_palette:
3906 inner.set_widget('center', palette_layout)
3907 widget = palette_layout
3908 else:
3909 widget = inner.get_widget('center')
3911 widget.set_aspect(1./golden_ratio)
3913 self.layout = inner
3915 return self.layout
3917 def draw_layout(self, layout):
3918 '''
3919 Use psxy to draw layout; for debugging
3920 '''
3922 # corners = layout.get_corners(descend=True)
3923 rects = num.array(layout.get_sizes(), dtype=float)
3924 rects_wid = rects[:, 0, 0]
3925 rects_hei = rects[:, 0, 1]
3926 rects_center_x = rects[:, 1, 0] + rects_wid*0.5
3927 rects_center_y = rects[:, 1, 1] + rects_hei*0.5
3928 nrects = len(rects)
3929 prects = (rects_center_x, rects_center_y, num.arange(nrects),
3930 num.zeros(nrects), rects_hei, rects_wid)
3932 # points = num.array(corners, dtype=float)
3934 cptfile = self.tempfilename() + '.cpt'
3935 self.makecpt(
3936 C='ocean',
3937 T='%g/%g/%g' % (-nrects, nrects, 1),
3938 Z=True,
3939 out_filename=cptfile, suppress_defaults=True)
3941 bb = layout.bbox()
3942 self.psxy(
3943 in_columns=prects,
3944 C=cptfile,
3945 W='1p',
3946 S='J',
3947 R=(bb[0], bb[2], bb[1], bb[3]),
3948 *layout.XYJ())
3951def simpleconf_to_ax(conf, axname):
3952 c = {}
3953 x = axname
3954 for x in ('', axname):
3955 for k in ('label', 'unit', 'scaled_unit', 'scaled_unit_factor',
3956 'space', 'mode', 'approx_ticks', 'limits', 'masking', 'inc',
3957 'snap'):
3959 if x+k in conf:
3960 c[k] = conf[x+k]
3962 return Ax(**c)
3965class DensityPlotDef(object):
3966 def __init__(self, data, cpt='ocean', tension=0.7, size=(640, 480),
3967 contour=False, method='surface', zscaler=None, **extra):
3968 self.data = data
3969 self.cpt = cpt
3970 self.tension = tension
3971 self.size = size
3972 self.contour = contour
3973 self.method = method
3974 self.zscaler = zscaler
3975 self.extra = extra
3978class TextDef(object):
3979 def __init__(
3980 self,
3981 data,
3982 size=9,
3983 justify='MC',
3984 fontno=0,
3985 offset=(0, 0),
3986 color='black'):
3988 self.data = data
3989 self.size = size
3990 self.justify = justify
3991 self.fontno = fontno
3992 self.offset = offset
3993 self.color = color
3996class Simple(object):
3997 def __init__(self, gmtconfig=None, gmtversion='newest', **simple_config):
3998 self.data = []
3999 self.symbols = []
4000 self.config = copy.deepcopy(simple_config)
4001 self.gmtconfig = gmtconfig
4002 self.density_plot_defs = []
4003 self.text_defs = []
4005 self.gmtversion = gmtversion
4007 self.data_x = []
4008 self.symbols_x = []
4010 self.data_y = []
4011 self.symbols_y = []
4013 self.default_config = {}
4014 self.set_defaults(width=15.*cm,
4015 height=15.*cm / golden_ratio,
4016 margins=(2.*cm, 2.*cm, 2.*cm, 2.*cm),
4017 with_palette=False,
4018 palette_offset=0.5*cm,
4019 palette_width=None,
4020 palette_height=None,
4021 zlabeloffset=2*cm,
4022 draw_layout=False)
4024 self.setup_defaults()
4025 self.fixate_widget_aspect = False
4027 def setup_defaults(self):
4028 pass
4030 def set_defaults(self, **kwargs):
4031 self.default_config.update(kwargs)
4033 def plot(self, data, symbol=''):
4034 self.data.append(data)
4035 self.symbols.append(symbol)
4037 def density_plot(self, data, **kwargs):
4038 dpd = DensityPlotDef(data, **kwargs)
4039 self.density_plot_defs.append(dpd)
4041 def text(self, data, **kwargs):
4042 dpd = TextDef(data, **kwargs)
4043 self.text_defs.append(dpd)
4045 def plot_x(self, data, symbol=''):
4046 self.data_x.append(data)
4047 self.symbols_x.append(symbol)
4049 def plot_y(self, data, symbol=''):
4050 self.data_y.append(data)
4051 self.symbols_y.append(symbol)
4053 def set(self, **kwargs):
4054 self.config.update(kwargs)
4056 def setup_base(self, conf):
4057 w = conf.pop('width')
4058 h = conf.pop('height')
4059 margins = conf.pop('margins')
4061 gmtconfig = {}
4062 if self.gmtconfig is not None:
4063 gmtconfig.update(self.gmtconfig)
4065 gmt = GMT(
4066 version=self.gmtversion,
4067 config=gmtconfig,
4068 config_papersize='Custom_%ix%i' % (w, h))
4070 layout = gmt.default_layout(with_palette=conf['with_palette'])
4071 layout.set_min_margins(*margins)
4072 if conf['with_palette']:
4073 widget = layout.get_widget().get_widget(0, 0)
4074 spacer = layout.get_widget().get_widget(1, 0)
4075 spacer.set_horizontal(conf['palette_offset'])
4076 palette_widget = layout.get_widget().get_widget(2, 0)
4077 if conf['palette_width'] is not None:
4078 palette_widget.set_horizontal(conf['palette_width'])
4079 if conf['palette_height'] is not None:
4080 palette_widget.set_vertical(conf['palette_height'])
4081 widget.set_vertical(h-margins[2]-margins[3]-0.03*cm)
4082 return gmt, layout, widget, palette_widget
4083 else:
4084 widget = layout.get_widget()
4085 return gmt, layout, widget, None
4087 def setup_projection(self, widget, scaler, conf):
4088 pass
4090 def setup_scaling(self, conf):
4091 ndims = 2
4092 if self.density_plot_defs:
4093 ndims = 3
4095 axes = [simpleconf_to_ax(conf, x) for x in 'xyz'[:ndims]]
4097 data_all = []
4098 data_all.extend(self.data)
4099 for dsd in self.density_plot_defs:
4100 if dsd.zscaler is None:
4101 data_all.append(dsd.data)
4102 else:
4103 data_all.append(dsd.data[:2])
4104 data_chopped = [ds[:ndims] for ds in data_all]
4106 scaler = ScaleGuru(data_chopped, axes=axes[:ndims])
4108 self.setup_scaling_plus(scaler, axes[:ndims])
4110 return scaler
4112 def setup_scaling_plus(self, scaler, axes):
4113 pass
4115 def setup_scaling_extra(self, scaler, conf):
4117 scaler_x = scaler.copy()
4118 scaler_x.data_ranges[1] = (0., 1.)
4119 scaler_x.axes[1].mode = 'off'
4121 scaler_y = scaler.copy()
4122 scaler_y.data_ranges[0] = (0., 1.)
4123 scaler_y.axes[0].mode = 'off'
4125 return scaler_x, scaler_y
4127 def draw_density(self, gmt, widget, scaler):
4129 R = scaler.R()
4130 # par = scaler.get_params()
4131 rxyj = R + widget.XYJ()
4132 innerticks = False
4133 for dpd in self.density_plot_defs:
4135 fn_cpt = gmt.tempfilename() + '.cpt'
4137 if dpd.zscaler is not None:
4138 s = dpd.zscaler
4139 else:
4140 s = scaler
4142 gmt.makecpt(C=dpd.cpt, out_filename=fn_cpt, *s.T())
4144 fn_grid = gmt.tempfilename()
4146 fn_mean = gmt.tempfilename()
4148 if dpd.method in ('surface', 'triangulate'):
4149 gmt.blockmean(in_columns=dpd.data,
4150 I='%i+/%i+' % dpd.size, # noqa
4151 out_filename=fn_mean, *R)
4153 if dpd.method == 'surface':
4154 gmt.surface(
4155 in_filename=fn_mean,
4156 T=dpd.tension,
4157 G=fn_grid,
4158 I='%i+/%i+' % dpd.size, # noqa
4159 out_discard=True,
4160 *R)
4162 if dpd.method == 'triangulate':
4163 gmt.triangulate(
4164 in_filename=fn_mean,
4165 G=fn_grid,
4166 I='%i+/%i+' % dpd.size, # noqa
4167 out_discard=True,
4168 V=True,
4169 *R)
4171 if gmt.is_gmt5():
4172 gmt.grdimage(fn_grid, C=fn_cpt, E='i', n='l', *rxyj)
4174 else:
4175 gmt.grdimage(fn_grid, C=fn_cpt, E='i', S='l', *rxyj)
4177 if dpd.contour:
4178 gmt.grdcontour(fn_grid, C=fn_cpt, W='0.5p,black', *rxyj)
4179 innerticks = '0.5p,black'
4181 os.remove(fn_grid)
4182 os.remove(fn_mean)
4184 if dpd.method == 'fillcontour':
4185 extra = dict(C=fn_cpt)
4186 extra.update(dpd.extra)
4187 gmt.pscontour(in_columns=dpd.data,
4188 I=True, *rxyj, **extra) # noqa
4190 if dpd.method == 'contour':
4191 extra = dict(W='0.5p,black', C=fn_cpt)
4192 extra.update(dpd.extra)
4193 gmt.pscontour(in_columns=dpd.data, *rxyj, **extra)
4195 return fn_cpt, innerticks
4197 def draw_basemap(self, gmt, widget, scaler):
4198 gmt.psbasemap(*(widget.JXY() + scaler.RB(ax_projection=True)))
4200 def draw(self, gmt, widget, scaler):
4201 rxyj = scaler.R() + widget.JXY()
4202 for dat, sym in zip(self.data, self.symbols):
4203 gmt.psxy(in_columns=dat, *(sym.split()+rxyj))
4205 def post_draw(self, gmt, widget, scaler):
4206 pass
4208 def pre_draw(self, gmt, widget, scaler):
4209 pass
4211 def draw_extra(self, gmt, widget, scaler_x, scaler_y):
4213 for dat, sym in zip(self.data_x, self.symbols_x):
4214 gmt.psxy(in_columns=dat,
4215 *(sym.split() + scaler_x.R() + widget.JXY()))
4217 for dat, sym in zip(self.data_y, self.symbols_y):
4218 gmt.psxy(in_columns=dat,
4219 *(sym.split() + scaler_y.R() + widget.JXY()))
4221 def draw_text(self, gmt, widget, scaler):
4223 rxyj = scaler.R() + widget.JXY()
4224 for td in self.text_defs:
4225 x, y = td.data[0:2]
4226 text = td.data[-1]
4227 size = td.size
4228 angle = 0
4229 fontno = td.fontno
4230 justify = td.justify
4231 color = td.color
4232 if gmt.is_gmt5():
4233 gmt.pstext(
4234 in_rows=[(x, y, text)],
4235 F='+f%gp,%s,%s+a%g+j%s' % (
4236 size, fontno, color, angle, justify),
4237 D='%gp/%gp' % td.offset, *rxyj)
4238 else:
4239 gmt.pstext(
4240 in_rows=[(x, y, size, angle, fontno, justify, text)],
4241 D='%gp/%gp' % td.offset, *rxyj)
4243 def save(self, filename, resolution=150):
4245 conf = dict(self.default_config)
4246 conf.update(self.config)
4248 gmt, layout, widget, palette_widget = self.setup_base(conf)
4249 scaler = self.setup_scaling(conf)
4250 scaler_x, scaler_y = self.setup_scaling_extra(scaler, conf)
4252 self.setup_projection(widget, scaler, conf)
4253 if self.fixate_widget_aspect:
4254 aspect = aspect_for_projection(
4255 gmt.installation['version'], *(widget.J() + scaler.R()))
4257 widget.set_aspect(aspect)
4259 if conf['draw_layout']:
4260 gmt.draw_layout(layout)
4261 cptfile = None
4262 if self.density_plot_defs:
4263 cptfile, innerticks = self.draw_density(gmt, widget, scaler)
4264 self.pre_draw(gmt, widget, scaler)
4265 self.draw(gmt, widget, scaler)
4266 self.post_draw(gmt, widget, scaler)
4267 self.draw_extra(gmt, widget, scaler_x, scaler_y)
4268 self.draw_text(gmt, widget, scaler)
4269 self.draw_basemap(gmt, widget, scaler)
4271 if palette_widget and cptfile:
4272 nice_palette(gmt, palette_widget, scaler, cptfile,
4273 innerticks=innerticks,
4274 zlabeloffset=conf['zlabeloffset'])
4276 gmt.save(filename, resolution=resolution)
4279class LinLinPlot(Simple):
4280 pass
4283class LogLinPlot(Simple):
4285 def setup_defaults(self):
4286 self.set_defaults(xmode='min-max')
4288 def setup_projection(self, widget, scaler, conf):
4289 widget['J'] = '-JX%(width)gpl/%(height)gp'
4290 scaler['B'] = '-B2:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen'
4293class LinLogPlot(Simple):
4295 def setup_defaults(self):
4296 self.set_defaults(ymode='min-max')
4298 def setup_projection(self, widget, scaler, conf):
4299 widget['J'] = '-JX%(width)gp/%(height)gpl'
4300 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/2:%(ylabel)s:WSen'
4303class LogLogPlot(Simple):
4305 def setup_defaults(self):
4306 self.set_defaults(mode='min-max')
4308 def setup_projection(self, widget, scaler, conf):
4309 widget['J'] = '-JX%(width)gpl/%(height)gpl'
4310 scaler['B'] = '-B2:%(xlabel)s:/2:%(ylabel)s:WSen'
4313class AziDistPlot(Simple):
4315 def __init__(self, *args, **kwargs):
4316 Simple.__init__(self, *args, **kwargs)
4317 self.fixate_widget_aspect = True
4319 def setup_defaults(self):
4320 self.set_defaults(
4321 height=15.*cm,
4322 width=15.*cm,
4323 xmode='off',
4324 xlimits=(0., 360.),
4325 xinc=45.)
4327 def setup_projection(self, widget, scaler, conf):
4328 widget['J'] = '-JPa%(width)gp'
4330 def setup_scaling_plus(self, scaler, axes):
4331 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/%(yinc)g:%(ylabel)s:N'
4334class MPlot(Simple):
4336 def __init__(self, *args, **kwargs):
4337 Simple.__init__(self, *args, **kwargs)
4338 self.fixate_widget_aspect = True
4340 def setup_defaults(self):
4341 self.set_defaults(xmode='min-max', ymode='min-max')
4343 def setup_projection(self, widget, scaler, conf):
4344 par = scaler.get_params()
4345 lon0 = (par['xmin'] + par['xmax'])/2.
4346 lat0 = (par['ymin'] + par['ymax'])/2.
4347 sll = '%g/%g' % (lon0, lat0)
4348 widget['J'] = '-JM' + sll + '/%(width)gp'
4349 scaler['B'] = \
4350 '-B%(xinc)gg%(xinc)g:%(xlabel)s:/%(yinc)gg%(yinc)g:%(ylabel)s:WSen'
4353def nice_palette(gmt, widget, scaleguru, cptfile, zlabeloffset=0.8*inch,
4354 innerticks=True):
4356 par = scaleguru.get_params()
4357 par_ax = scaleguru.get_params(ax_projection=True)
4358 nz_palette = int(widget.height()/inch * 300)
4359 px = num.zeros(nz_palette*2)
4360 px[1::2] += 1
4361 pz = num.linspace(par['zmin'], par['zmax'], nz_palette).repeat(2)
4362 pdz = pz[2]-pz[0]
4363 palgrdfile = gmt.tempfilename()
4364 pal_r = (0, 1, par['zmin'], par['zmax'])
4365 pal_ax_r = (0, 1, par_ax['zmin'], par_ax['zmax'])
4366 gmt.xyz2grd(
4367 G=palgrdfile, R=pal_r,
4368 I=(1, pdz), in_columns=(px, pz, pz), # noqa
4369 out_discard=True)
4371 gmt.grdimage(palgrdfile, R=pal_r, C=cptfile, *widget.JXY())
4372 if isinstance(innerticks, str):
4373 tickpen = innerticks
4374 gmt.grdcontour(palgrdfile, W=tickpen, R=pal_r, C=cptfile,
4375 *widget.JXY())
4377 negpalwid = '%gp' % -widget.width()
4378 if not isinstance(innerticks, str) and innerticks:
4379 ticklen = negpalwid
4380 else:
4381 ticklen = '0p'
4383 TICK_LENGTH_PARAM = 'MAP_TICK_LENGTH' if gmt.is_gmt5() else 'TICK_LENGTH'
4384 gmt.psbasemap(
4385 R=pal_ax_r, B='4::/%(zinc)g::nsw' % par_ax,
4386 config={TICK_LENGTH_PARAM: ticklen},
4387 *widget.JXY())
4389 if innerticks:
4390 gmt.psbasemap(
4391 R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax,
4392 config={TICK_LENGTH_PARAM: '0p'},
4393 *widget.JXY())
4394 else:
4395 gmt.psbasemap(R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax, *widget.JXY())
4397 if par_ax['zlabel']:
4398 label_font = gmt.label_font()
4399 label_font_size = gmt.label_font_size()
4400 label_offset = zlabeloffset
4401 gmt.pstext(
4402 R=(0, 1, 0, 2), D="%gp/0p" % label_offset,
4403 N=True,
4404 in_rows=[(1, 1, label_font_size, -90, label_font, 'CB',
4405 par_ax['zlabel'])],
4406 *widget.JXY())