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