1# http://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

4# ---|P------/S----------~Lg---------- 

5''' 

6A Python interface to GMT. 

7''' 

8 

9# This file is part of GmtPy (http://emolch.github.io/gmtpy/) 

10# See there for copying and licensing information. 

11 

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 

34 

35from pyrocko import ExternalProgramMissing 

36 

37try: 

38 newstr = unicode 

39except NameError: 

40 newstr = str 

41 

42find_bb = re.compile(br'%%BoundingBox:((\s+[-0-9]+){4})') 

43find_hiresbb = re.compile(br'%%HiResBoundingBox:((\s+[-0-9.]+){4})') 

44 

45 

46encoding_gmt_to_python = { 

47 'isolatin1+': 'iso-8859-1', 

48 'standard+': 'ascii', 

49 'isolatin1': 'iso-8859-1', 

50 'standard': 'ascii'} 

51 

52for i in range(1, 11): 

53 encoding_gmt_to_python['iso-8859-%i' % i] = 'iso-8859-%i' % i 

54 

55 

56def have_gmt(): 

57 try: 

58 get_gmt_installation('newest') 

59 return True 

60 

61 except GMTInstallationProblem: 

62 return False 

63 

64 

65def check_have_gmt(): 

66 if not have_gmt(): 

67 raise ExternalProgramMissing('GMT is not installed or cannot be found') 

68 

69 

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) 

76 

77 (stdout, stderr) = p.communicate() 

78 

79 except OSError: 

80 return False 

81 

82 return True 

83 

84 

85class GmtPyError(Exception): 

86 pass 

87 

88 

89class GMTError(GmtPyError): 

90 pass 

91 

92 

93class GMTInstallationProblem(GmtPyError): 

94 pass 

95 

96 

97def convert_graph(in_filename, out_filename, resolution=75., oversample=2., 

98 width=None, height=None, size=None): 

99 

100 _, tmp_filename_base = tempfile.mkstemp() 

101 

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' 

110 

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))] 

119 

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)) 

127 

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)) 

138 

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)) 

150 

151 except Exception: 

152 raise 

153 

154 finally: 

155 if os.path.exists(tmp_filename_base): 

156 os.remove(tmp_filename_base) 

157 

158 if os.path.exists(tmp_filename): 

159 os.remove(tmp_filename) 

160 

161 

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 

168 

169 raise GmtPyError('Cannot find bbox') 

170 

171 

172def replace_bbox(bbox, *args): 

173 

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') 

184 

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) 

189 

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 

199 

200 if nn == 2: 

201 for line in fin: 

202 fout.write(line) 

203 

204 

205def escape_shell_arg(s): 

206 ''' 

207 This function should be used for debugging output only - it could be 

208 insecure. 

209 ''' 

210 

211 if re.search(r'[^a-zA-Z0-9._/=-]', s): 

212 return "'" + s.replace("'", "'\\''") + "'" 

213 else: 

214 return s 

215 

216 

217def escape_shell_args(args): 

218 ''' 

219 This function should be used for debugging output only - it could be 

220 insecure. 

221 ''' 

222 

223 return ' '.join([escape_shell_arg(x) for x in args]) 

224 

225 

226golden_ratio = 1.61803 

227 

228# units in points 

229_units = { 

230 'i': 72., 

231 'c': 72./2.54, 

232 'm': 72.*100./2.54, 

233 'p': 1.} 

234 

235inch = _units['i'] 

236cm = _units['c'] 

237 

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} 

268 

269graph_colors = [tango_colors[_x] for _x in ( 

270 'scarletred2', 'skyblue3', 'chameleon3', 'orange2', 'plum2', 'chocolate2', 

271 'butter2')] 

272 

273 

274def color(x=None): 

275 ''' 

276 Generate a string for GMT option arguments expecting a color. 

277 

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 ''' 

284 

285 if x is None: 

286 return '%i/%i/%i' % tuple(random.randint(0, 255) for _ in 'rgb') 

287 

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' 

293 

294 elif isinstance(x, str): 

295 if x in tango_colors: 

296 return '%i/%i/%i' % tango_colors[x] 

297 else: 

298 return x 

299 

300 return '%i/%i/%i' % x 

301 

302 

303def color_tup(x=None): 

304 if x is None: 

305 return tuple([random.randint(0, 255) for _x in 'rgb']) 

306 

307 if isinstance(x, int): 

308 if 0 <= x < len(graph_colors): 

309 return graph_colors[x] 

310 else: 

311 return (0, 0, 0) 

312 

313 elif isinstance(x, str): 

314 if x in tango_colors: 

315 return tango_colors[x] 

316 

317 return x 

318 

319 

320_gmt_installations = {} 

321 

322# Set fixed installation(s) to use... 

323# (use this, if you want to use different GMT versions simultaneously.) 

324 

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' } 

331 

332# ... or let GmtPy autodetect GMT via $PATH and $GMTHOME 

333 

334 

335def key_version(a): 

336 a = a.split('_')[0] # get rid of revision id 

337 return [int(x) for x in a.split('.')] 

338 

339 

340def newest_installed_gmt_version(): 

341 return sorted(_gmt_installations.keys(), key=key_version)[-1] 

342 

343 

344def all_installed_gmt_versions(): 

345 return sorted(_gmt_installations.keys(), key=key_version) 

346 

347 

348# To have consistent defaults, they are hardcoded here and should not be 

349# changed. 

350 

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''' 

458 

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''' 

565 

566 

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''' 

673 

674 

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''' 

782 

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''' 

892 

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''' 

1002 

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''' 

1142 

1143 

1144def get_gmt_version(gmtdefaultsbinary, gmthomedir=None): 

1145 args = [gmtdefaultsbinary] 

1146 

1147 environ = os.environ.copy() 

1148 environ['GMTHOME'] = gmthomedir or '' 

1149 

1150 p = subprocess.Popen( 

1151 args, 

1152 stdout=subprocess.PIPE, 

1153 stderr=subprocess.PIPE, 

1154 env=environ) 

1155 

1156 (stdout, stderr) = p.communicate() 

1157 m = re.search(br'(\d+(\.\d+)*)', stderr) \ 

1158 or re.search(br'# GMT (\d+(\.\d+)*)', stdout) 

1159 

1160 if not m: 

1161 raise GMTInstallationProblem( 

1162 "Can't extract version number from output of %s." 

1163 % gmtdefaultsbinary) 

1164 

1165 return str(m.group(1).decode('ascii')) 

1166 

1167 

1168def detect_gmt_installations(): 

1169 

1170 installations = {} 

1171 errmesses = [] 

1172 

1173 # GMT 4.x: 

1174 try: 

1175 p = subprocess.Popen( 

1176 ['GMT'], 

1177 stdout=subprocess.PIPE, 

1178 stderr=subprocess.PIPE) 

1179 

1180 (stdout, stderr) = p.communicate() 

1181 

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.") 

1186 

1187 version = str(m.group(1).decode('ascii')) 

1188 if version[0] != '5': 

1189 

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.") 

1194 

1195 gmtbin = str(m.group(1).decode('ascii')) 

1196 

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.") 

1201 

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.") 

1206 

1207 gmthome = gmtshare[:-6] 

1208 

1209 installations[version] = { 

1210 'home': gmthome, 

1211 'bin': gmtbin} 

1212 

1213 except OSError as e: 

1214 errmesses.append(('GMT', str(e))) 

1215 

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} 

1223 

1224 except (OSError, subprocess.CalledProcessError) as e: 

1225 errmesses.append(('gmt', str(e))) 

1226 

1227 if not installations: 

1228 s = [] 

1229 for (progname, errmess) in errmesses: 

1230 s.append('Cannot start "%s" executable: %s' % (progname, errmess)) 

1231 

1232 raise GMTInstallationProblem(', '.join(s)) 

1233 

1234 return installations 

1235 

1236 

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 

1242 

1243 elif key_version(version) < key_version(avail): 

1244 return avails[max(0, iavail-1)] 

1245 

1246 return avails[-1] 

1247 

1248 

1249def gmt_default_config(version): 

1250 ''' 

1251 Get default GMT configuration dict for given version. 

1252 ''' 

1253 

1254 xversion = appropriate_defaults_version(version) 

1255 

1256 # if not version in _gmt_defaults_by_version: 

1257 # raise GMTError('No GMT defaults for version %s found' % version) 

1258 

1259 gmt_defaults = _gmt_defaults_by_version[xversion] 

1260 

1261 d = {} 

1262 for line in gmt_defaults.splitlines(): 

1263 sline = line.strip() 

1264 if not sline or sline.startswith('#'): 

1265 continue 

1266 

1267 k, v = sline.split('=', 1) 

1268 d[k.strip()] = v.strip() 

1269 

1270 return d 

1271 

1272 

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])) 

1283 

1284 for k in d2: 

1285 if k not in d1: 

1286 print('%s not in %s' % (k, v1)) 

1287 

1288# diff_defaults('4.5.2', '4.5.3') 

1289 

1290 

1291def check_gmt_installation(installation): 

1292 

1293 home_dir = installation.get('home', None) 

1294 bin_dir = installation['bin'] 

1295 version = installation['version'] 

1296 

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) 

1302 

1303 major_version = version.split('.')[0] 

1304 

1305 if major_version not in ['5', '6']: 

1306 gmtdefaults = pjoin(bin_dir, 'gmtdefaults') 

1307 

1308 versionfound = get_gmt_version(gmtdefaults, home_dir) 

1309 

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)) 

1315 

1316 

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())) 

1322 

1323 version = 'newest' 

1324 

1325 if version == 'newest': 

1326 version = newest_installed_gmt_version() 

1327 

1328 installation = dict(_gmt_installations[version]) 

1329 

1330 return installation 

1331 

1332 

1333def setup_gmt_installations(): 

1334 if not setup_gmt_installations.have_done: 

1335 if not _gmt_installations: 

1336 

1337 _gmt_installations.update(detect_gmt_installations()) 

1338 

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 

1343 

1344 for installation in _gmt_installations.values(): 

1345 check_gmt_installation(installation) 

1346 

1347 setup_gmt_installations.have_done = True 

1348 

1349 

1350setup_gmt_installations.have_done = False 

1351 

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''' 

1381 

1382 

1383_paper_sizes = {} 

1384 

1385 

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) 

1391 

1392 

1393def get_paper_size(k): 

1394 setup_paper_sizes() 

1395 return _paper_sizes[k.lower().rstrip('+')] 

1396 

1397 

1398def all_paper_sizes(): 

1399 setup_paper_sizes() 

1400 return _paper_sizes 

1401 

1402 

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] 

1407 

1408 raise GmtPyError('cannot get measure unit / proj length unit from config') 

1409 

1410 

1411def paper_media(gmt_config): 

1412 for k in ['PAPER_MEDIA', 'PS_MEDIA']: 

1413 if k in gmt_config: 

1414 return gmt_config[k] 

1415 

1416 raise GmtPyError('cannot get paper media from config') 

1417 

1418 

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] 

1423 

1424 raise GmtPyError('cannot get paper orientation from config') 

1425 

1426 

1427def make_bbox(width, height, gmt_config, margins=(0.8, 0.8, 0.8, 0.8)): 

1428 

1429 leftmargin, topmargin, rightmargin, bottommargin = margins 

1430 portrait = page_orientation(gmt_config).lower() == 'portrait' 

1431 

1432 paper_size = get_paper_size(paper_media(gmt_config)) 

1433 if not portrait: 

1434 paper_size = paper_size[1], paper_size[0] 

1435 

1436 xoffset = (paper_size[0] - (width + leftmargin + rightmargin)) / \ 

1437 2.0 + leftmargin 

1438 yoffset = (paper_size[1] - (height + topmargin + bottommargin)) / \ 

1439 2.0 + bottommargin 

1440 

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)) 

1451 

1452 return xoffset, yoffset, (bb1, bb2, bb3, bb4) 

1453 

1454 

1455def gmtdefaults_as_text(version='newest'): 

1456 

1457 ''' 

1458 Get the built-in gmtdefaults. 

1459 ''' 

1460 

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' 

1465 

1466 if version == 'newest': 

1467 version = newest_installed_gmt_version() 

1468 

1469 return _gmt_defaults_by_version[version] 

1470 

1471 

1472def savegrd(x, y, z, filename, title=None, naming='xy'): 

1473 ''' 

1474 Write COARDS compliant netcdf (grd) file. 

1475 ''' 

1476 

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') 

1481 

1482 if naming == 'xy': 

1483 kx, ky = 'x', 'y' 

1484 else: 

1485 kx, ky = 'lon', 'lat' 

1486 

1487 nc.node_offset = 0 

1488 if title is not None: 

1489 nc.title = title 

1490 

1491 nc.Conventions = 'COARDS/CF-1.0' 

1492 nc.createDimension(kx, nx) 

1493 nc.createDimension(ky, ny) 

1494 

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' 

1505 

1506 zvar = nc.createVariable('z', 'd', (ky, kx)) 

1507 

1508 xvar[:] = x.astype(num.float64) 

1509 yvar[:] = y.astype(num.float64) 

1510 zvar[:] = z.astype(num.float64) 

1511 

1512 nc.close() 

1513 

1514 

1515def to_array(var): 

1516 arr = var[:].copy() 

1517 if hasattr(var, 'scale_factor'): 

1518 arr *= var.scale_factor 

1519 

1520 if hasattr(var, 'add_offset'): 

1521 arr += var.add_offset 

1522 

1523 return arr 

1524 

1525 

1526def loadgrd(filename): 

1527 ''' 

1528 Read COARDS compliant netcdf (grd) file. 

1529 ''' 

1530 

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' 

1539 

1540 kz = 'z' 

1541 if 'altitude' in vkeys: 

1542 kz = 'altitude' 

1543 

1544 x = to_array(nc.variables[kx]) 

1545 y = to_array(nc.variables[ky]) 

1546 z = to_array(nc.variables[kz]) 

1547 

1548 nc.close() 

1549 return x, y, z 

1550 

1551 

1552def centers_to_edges(asorted): 

1553 return (asorted[1:] + asorted[:-1])/2. 

1554 

1555 

1556def nvals(asorted): 

1557 eps = (asorted[-1]-asorted[0])/asorted.size 

1558 return num.sum(asorted[1:] - asorted[:-1] >= eps) + 1 

1559 

1560 

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]) 

1569 

1570 

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])) 

1580 

1581 

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)) 

1586 

1587 zindi = yindi*nx+xindi 

1588 order = num.argsort(zindi) 

1589 z = z[order] 

1590 zindi = zindi[order] 

1591 

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) 

1597 

1598 

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 

1616 

1617 

1618def griddata_auto(x, y, z, mode=None): 

1619 ''' 

1620 Grid tabular XYZ data by binning. 

1621 

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). 

1627 

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 ''' 

1632 

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) 

1645 

1646 return xf, yf, zf 

1647 

1648 

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 

1655 

1656 

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 

1662 

1663 

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 

1677 

1678 

1679def doublegrid(x, y, z): 

1680 x2 = double1d(x) 

1681 y2 = double1d(y) 

1682 z2 = double2d(z) 

1683 return x2, y2, z2 

1684 

1685 

1686class Guru(object): 

1687 ''' 

1688 Abstract base class providing template interpolation, accessible as 

1689 attributes. 

1690 

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 ''' 

1696 

1697 def __init__(self): 

1698 self.templates = {} 

1699 

1700 def fill(self, templates, **kwargs): 

1701 params = self.get_params(**kwargs) 

1702 strings = [t % params for t in templates] 

1703 return strings 

1704 

1705 # hand through templates dict 

1706 def __getitem__(self, template_name): 

1707 return self.templates[template_name] 

1708 

1709 def __setitem__(self, template_name, template): 

1710 self.templates[template_name] = template 

1711 

1712 def __contains__(self, template_name): 

1713 return template_name in self.templates 

1714 

1715 def __iter__(self): 

1716 return iter(self.templates) 

1717 

1718 def __len__(self): 

1719 return len(self.templates) 

1720 

1721 def __delitem__(self, template_name): 

1722 del self.templates[template_name] 

1723 

1724 def _simple_fill(self, template_names, **kwargs): 

1725 templates = [self.templates[n] for n in template_names] 

1726 return self.fill(templates, **kwargs) 

1727 

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) 

1731 

1732 def f(**kwargs): 

1733 return self._simple_fill(template_names, **kwargs) 

1734 

1735 return f 

1736 

1737 

1738def nice_value(x): 

1739 ''' 

1740 Round ``x`` to nice value. 

1741 ''' 

1742 

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 

1754 

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 

1763 

1764 return sign * 0.1 * exp 

1765 

1766 

1767class AutoScaler(object): 

1768 ''' 

1769 Tunable 1D autoscaling based on data range. 

1770 

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. 

1774 

1775 The autoscaling process is guided by the following public attributes: 

1776 

1777 .. py:attribute:: approx_ticks 

1778 

1779 Approximate number of increment steps (tickmarks) to generate. 

1780 

1781 .. py:attribute:: mode 

1782 

1783 Mode of operation: one of ``'auto'``, ``'min-max'``, ``'0-max'``, 

1784 ``'min-0'``, ``'symmetric'`` or ``'off'``. 

1785 

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 ================ ================================================== 

1801 

1802 .. py:attribute:: exp 

1803 

1804 If defined, override automatically determined exponent for notation 

1805 by the given value. 

1806 

1807 .. py:attribute:: snap 

1808 

1809 If set to True, snap output range to multiples of increment. This 

1810 parameter has no effect, if mode is set to ``'off'``. 

1811 

1812 .. py:attribute:: inc 

1813 

1814 If defined, override automatically determined tick increment by the 

1815 given value. 

1816 

1817 .. py:attribute:: space 

1818 

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'``. 

1823 

1824 .. py:attribute:: exp_factor 

1825 

1826 Exponent of notation is chosen to be a multiple of this value. 

1827 

1828 .. py:attribute:: no_exp_interval: 

1829 

1830 Range of exponent, for which no exponential notation is allowed. 

1831 

1832 ''' 

1833 

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)): 

1844 

1845 ''' 

1846 Create new AutoScaler instance. 

1847 

1848 The parameters are described in the AutoScaler documentation. 

1849 ''' 

1850 

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 

1859 

1860 def make_scale(self, data_range, override_mode=None): 

1861 

1862 ''' 

1863 Get nice minimum, maximum and increment for given data range. 

1864 

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 ''' 

1871 

1872 data_min = min(data_range) 

1873 data_max = max(data_range) 

1874 

1875 is_reverse = (data_range[0] > data_range[1]) 

1876 

1877 a = self.mode 

1878 if self.mode == 'auto': 

1879 a = self.guess_autoscale_mode(data_min, data_max) 

1880 

1881 if override_mode is not None: 

1882 a = override_mode 

1883 

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 

1905 

1906 nmi = mi 

1907 if (mi != 0. or a == 'min-max') and a != 'off': 

1908 nmi = mi - self.space*(ma-mi) 

1909 

1910 nma = ma 

1911 if (ma != 0. or a == 'min-max') and a != 'off': 

1912 nma = ma + self.space*(ma-mi) 

1913 

1914 mi, ma = nmi, nma 

1915 

1916 if mi == ma and a != 'off': 

1917 mi -= 1.0 

1918 ma += 1.0 

1919 

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.) 

1928 

1929 if inc == 0.0: 

1930 inc = 1.0 

1931 

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) 

1936 

1937 if is_reverse: 

1938 return ma, mi, -inc 

1939 else: 

1940 return mi, ma, inc 

1941 

1942 def make_exp(self, x): 

1943 ''' 

1944 Get nice exponent for notation of ``x``. 

1945 

1946 For ax annotations, give tick increment as ``x``. 

1947 ''' 

1948 

1949 if self.exp is not None: 

1950 return self.exp 

1951 

1952 x = abs(x) 

1953 if x == 0.0: 

1954 return 0 

1955 

1956 if 10**self.no_exp_interval[0] <= x <= 10**self.no_exp_interval[1]: 

1957 return 0 

1958 

1959 return math.floor(math.log10(x)/self.exp_factor)*self.exp_factor 

1960 

1961 def guess_autoscale_mode(self, data_min, data_max): 

1962 ''' 

1963 Guess mode of operation, based on data range. 

1964 

1965 Used to map ``'auto'`` mode to ``'0-max'``, ``'min-0'``, ``'min-max'`` 

1966 or ``'symmetric'``. 

1967 ''' 

1968 

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 

1987 

1988 

1989class Ax(AutoScaler): 

1990 ''' 

1991 Ax description with autoscaling capabilities. 

1992 

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): 

1996 

1997 .. py:attribute:: label 

1998 

1999 Ax label (without unit). 

2000 

2001 .. py:attribute:: unit 

2002 

2003 Physical unit of the data attached to this ax. 

2004 

2005 .. py:attribute:: scaled_unit 

2006 

2007 (see below) 

2008 

2009 .. py:attribute:: scaled_unit_factor 

2010 

2011 Scaled physical unit and factor between unit and scaled_unit so that 

2012 

2013 unit = scaled_unit_factor x scaled_unit. 

2014 

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.) 

2018 

2019 .. py:attribute:: limits 

2020 

2021 If defined, fix range of ax to limits=(min,max). 

2022 

2023 .. py:attribute:: masking 

2024 

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. 

2028 

2029 ''' 

2030 

2031 def __init__(self, label='', unit='', scaled_unit_factor=1., 

2032 scaled_unit='', limits=None, masking=True, **kwargs): 

2033 

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 

2041 

2042 def label_str(self, exp, unit): 

2043 ''' 

2044 Get label string including the unit and multiplier. 

2045 ''' 

2046 

2047 slabel, sunit, sexp = '', '', '' 

2048 if self.label: 

2049 slabel = self.label 

2050 

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 

2057 

2058 p = [] 

2059 if slabel: 

2060 p.append(slabel) 

2061 

2062 if sunit: 

2063 p.append(sunit) 

2064 

2065 return ' '.join(p) 

2066 

2067 def make_params(self, data_range, ax_projection=False, override_mode=None, 

2068 override_scaled_unit_factor=None): 

2069 

2070 ''' 

2071 Get minimum, maximum, increment and label string for ax display.' 

2072 

2073 Returns minimum, maximum, increment and label string including unit and 

2074 multiplier for given data range. 

2075 

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 ''' 

2081 

2082 sf = self.scaled_unit_factor 

2083 

2084 if override_scaled_unit_factor is not None: 

2085 sf = override_scaled_unit_factor 

2086 

2087 dr_scaled = [sf*x for x in data_range] 

2088 

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 

2092 

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 

2104 

2105 

2106class ScaleGuru(Guru): 

2107 

2108 ''' 

2109 2D/3D autoscaling and ax annotation facility. 

2110 

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. 

2115 

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.: 

2119 

2120 * The ability to impose a fixed aspect ratio between two axes. 

2121 

2122 * Recalculation of data range on non-limited axes, when there are 

2123 limits imposed on other axes. 

2124 

2125 ''' 

2126 

2127 def __init__(self, data_tuples=None, axes=None, aspect=None, 

2128 percent_interval=None, copy_from=None): 

2129 

2130 Guru.__init__(self) 

2131 

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 

2137 

2138 if percent_interval is not None: 

2139 from scipy.stats import scoreatpercentile as scap 

2140 

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') 

2145 

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)] 

2157 

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])) 

2173 

2174 for i, ax, x in zip(range(maxdim), self.axes, dt): 

2175 

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.) 

2205 

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]) 

2210 

2211 if ax.limits[1] is not None: 

2212 range_this = min(ax.limits[1], 

2213 range_this[0]), ax.limits[1] 

2214 

2215 else: 

2216 range_this = ax.limits 

2217 

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) 

2225 

2226 data_ranges[i] = (mi, ma) 

2227 

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])): 

2232 

2233 data_ranges[i] = (0., 1.) 

2234 

2235 self.data_ranges = data_ranges 

2236 self.aspect = aspect 

2237 

2238 def copy(self): 

2239 return ScaleGuru(copy_from=self) 

2240 

2241 def get_params(self, ax_projection=False): 

2242 

2243 ''' 

2244 Get dict with output parameters. 

2245 

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'``. 

2250 

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 ''' 

2259 

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) 

2267 

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.) 

2278 

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.) 

2285 

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)) 

2290 

2291 return params 

2292 

2293 

2294class GumSpring(object): 

2295 

2296 ''' 

2297 Sizing policy implementing a minimal size, plus a desire to grow. 

2298 ''' 

2299 

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 

2310 

2311 def get_minimal(self): 

2312 if self.minimal is not None: 

2313 return self.minimal 

2314 else: 

2315 return 0.0 

2316 

2317 def get_grow(self): 

2318 return self.grow 

2319 

2320 def set_value(self, value): 

2321 self.value = value 

2322 

2323 def get_value(self): 

2324 return self.value 

2325 

2326 

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 

2334 

2335 

2336class Widget(Guru): 

2337 

2338 ''' 

2339 Base class of the gmtpy layout system. 

2340 

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. 

2344 

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. 

2351 

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. 

2361 

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 ''' 

2366 

2367 def __init__(self, horizontal=None, vertical=None, parent=None): 

2368 

2369 ''' 

2370 Create new widget. 

2371 ''' 

2372 

2373 Guru.__init__(self) 

2374 

2375 self.templates = dict( 

2376 X='-Xa%(xoffset)gp', 

2377 Y='-Ya%(yoffset)gp', 

2378 J='-JX%(width)gp/%(height)gp') 

2379 

2380 if horizontal is None: 

2381 self.horizontal = GumSpring() 

2382 else: 

2383 self.horizontal = horizontal 

2384 

2385 if vertical is None: 

2386 self.vertical = GumSpring() 

2387 else: 

2388 self.vertical = vertical 

2389 

2390 self.aspect = None 

2391 self.parent = parent 

2392 self.dirty = True 

2393 

2394 def set_parent(self, parent): 

2395 

2396 ''' 

2397 Set the parent widget. 

2398 

2399 This method should not be called directly. The :py:meth:`set_widget` 

2400 methods are responsible for calling this. 

2401 ''' 

2402 

2403 self.parent = parent 

2404 self.dirtyfy() 

2405 

2406 def get_parent(self): 

2407 

2408 ''' 

2409 Get the widgets parent widget. 

2410 ''' 

2411 

2412 return self.parent 

2413 

2414 def get_root(self): 

2415 

2416 ''' 

2417 Get the root widget in the layout hierarchy. 

2418 ''' 

2419 

2420 if self.parent is not None: 

2421 return self.get_parent() 

2422 else: 

2423 return self 

2424 

2425 def set_horizontal(self, minimal=None, grow=None): 

2426 

2427 ''' 

2428 Set the horizontal sizing policy of the Widget. 

2429 

2430 

2431 :param minimal: new minimal width of the widget 

2432 :param grow: new horizontal grow disire of the widget 

2433 ''' 

2434 

2435 self.horizontal = GumSpring(minimal, grow) 

2436 self.dirtyfy() 

2437 

2438 def get_horizontal(self): 

2439 return self.horizontal.get_minimal(), self.horizontal.get_grow() 

2440 

2441 def set_vertical(self, minimal=None, grow=None): 

2442 

2443 ''' 

2444 Set the horizontal sizing policy of the Widget. 

2445 

2446 :param minimal: new minimal height of the widget 

2447 :param grow: new vertical grow disire of the widget 

2448 ''' 

2449 

2450 self.vertical = GumSpring(minimal, grow) 

2451 self.dirtyfy() 

2452 

2453 def get_vertical(self): 

2454 return self.vertical.get_minimal(), self.vertical.get_grow() 

2455 

2456 def set_aspect(self, aspect=None): 

2457 

2458 ''' 

2459 Set aspect constraint on the widget. 

2460 

2461 The aspect is given as height divided by width. 

2462 ''' 

2463 

2464 self.aspect = aspect 

2465 self.dirtyfy() 

2466 

2467 def set_policy(self, minimal=(None, None), grow=(None, None), aspect=None): 

2468 

2469 ''' 

2470 Shortcut to set sizing and aspect constraints in a single method 

2471 call. 

2472 ''' 

2473 

2474 self.set_horizontal(minimal[0], grow[0]) 

2475 self.set_vertical(minimal[1], grow[1]) 

2476 self.set_aspect(aspect) 

2477 

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 

2482 

2483 def legalize(self, size, offset): 

2484 

2485 ''' 

2486 Get legal size for widget. 

2487 

2488 Returns: (new_size, new_offset) 

2489 

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 ''' 

2494 

2495 sh, sv = size 

2496 oh, ov = offset 

2497 shs, svs = Widget.get_min_size(self) 

2498 ghs, gvs = Widget.get_grow(self) 

2499 

2500 if ghs == 0.0: 

2501 oh += (sh-shs)/2. 

2502 sh = shs 

2503 

2504 if gvs == 0.0: 

2505 ov += (sv-svs)/2. 

2506 sv = svs 

2507 

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 

2515 

2516 return (sh, sv), (oh, ov) 

2517 

2518 def get_min_size(self): 

2519 

2520 ''' 

2521 Get minimum size of widget. 

2522 

2523 Used by the layout managers. Should be overloaded in derived classes. 

2524 ''' 

2525 

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 

2533 

2534 def get_grow(self): 

2535 

2536 ''' 

2537 Get widget's desire to grow. 

2538 

2539 Used by the layout managers. Should be overloaded in derived classes. 

2540 ''' 

2541 

2542 return self.horizontal.get_grow(), self.vertical.get_grow() 

2543 

2544 def set_size(self, size, offset): 

2545 

2546 ''' 

2547 Set the widget's current size. 

2548 

2549 Should not be called directly. It is the layout manager's 

2550 responsibility to call this. 

2551 ''' 

2552 

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 

2558 

2559 def __str__(self): 

2560 

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 

2569 

2570 def policies_debug_str(self): 

2571 

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,)) 

2577 

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 

2583 

2584 def get_corners(self, descend=False): 

2585 

2586 ''' 

2587 Get coordinates of the corners of the widget. 

2588 

2589 Returns list with coordinate tuples. 

2590 

2591 If ``descend`` is True, the returned list will contain corner 

2592 coordinates of all sub-widgets. 

2593 ''' 

2594 

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 

2602 

2603 def get_sizes(self): 

2604 

2605 ''' 

2606 Get sizes of this widget and all it's children. 

2607 

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 

2615 

2616 def do_layout(self): 

2617 

2618 ''' 

2619 Triggers layouting of the widget hierarchy, if needed. 

2620 ''' 

2621 

2622 if self.parent is not None: 

2623 return self.parent.do_layout() 

2624 

2625 if not self.dirty: 

2626 return 

2627 

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.)) 

2635 

2636 def get_children(self): 

2637 

2638 ''' 

2639 Get sub-widgets contained in this widget. 

2640 

2641 Returns a list of widgets. 

2642 ''' 

2643 

2644 return [] 

2645 

2646 def get_size(self): 

2647 

2648 ''' 

2649 Get current size and position of the widget. 

2650 

2651 Triggers layouting and returns 

2652 ``((width, height), (xoffset, yoffset))`` 

2653 ''' 

2654 

2655 self.do_layout() 

2656 return (self.horizontal.get_value(), 

2657 self.vertical.get_value()), self.offset 

2658 

2659 def get_params(self): 

2660 

2661 ''' 

2662 Get current size and position of the widget. 

2663 

2664 Triggers layouting and returns dict with keys ``'xoffset'``, 

2665 ``'yoffset'``, ``'width'`` and ``'height'``. 

2666 ''' 

2667 

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']) 

2672 

2673 def width(self): 

2674 

2675 ''' 

2676 Get current width of the widget. 

2677 

2678 Triggers layouting and returns width. 

2679 ''' 

2680 

2681 self.do_layout() 

2682 return self.horizontal.get_value() 

2683 

2684 def height(self): 

2685 

2686 ''' 

2687 Get current height of the widget. 

2688 

2689 Triggers layouting and return height. 

2690 ''' 

2691 

2692 self.do_layout() 

2693 return self.vertical.get_value() 

2694 

2695 def bbox(self): 

2696 

2697 ''' 

2698 Get PostScript bounding box for this widget. 

2699 

2700 Triggers layouting and returns values suitable to create PS bounding 

2701 box, representing the widgets current size and position. 

2702 ''' 

2703 

2704 self.do_layout() 

2705 return (self.offset[0], self.offset[1], self.offset[0]+self.width(), 

2706 self.offset[1]+self.height()) 

2707 

2708 def dirtyfy(self): 

2709 

2710 ''' 

2711 Set dirty flag on top level widget in the hierarchy. 

2712 

2713 Called by various methods, to indicate, that the widget hierarchy needs 

2714 new layouting. 

2715 ''' 

2716 

2717 if self.parent is not None: 

2718 self.parent.dirtyfy() 

2719 

2720 self.dirty = True 

2721 

2722 

2723class CenterLayout(Widget): 

2724 

2725 ''' 

2726 A layout manager which centers its single child widget. 

2727 

2728 The child widget may be oversized. 

2729 ''' 

2730 

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) 

2735 

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) 

2740 

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 

2745 

2746 def set_size(self, size, offset): 

2747 (sh, sv), (oh, ov) = self.legalize(size, offset) 

2748 

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. 

2757 

2758 self.content.set_size((shc, svc), (ohc, ovc)) 

2759 Widget.set_size(self, (sh, sv), (oh, ov)) 

2760 

2761 def set_widget(self, widget=None): 

2762 

2763 ''' 

2764 Set the child widget, which shall be centered. 

2765 ''' 

2766 

2767 if widget is None: 

2768 widget = Widget() 

2769 

2770 self.content = widget 

2771 

2772 widget.set_parent(self) 

2773 

2774 def get_widget(self): 

2775 return self.content 

2776 

2777 def get_children(self): 

2778 return [self.content] 

2779 

2780 

2781class FrameLayout(Widget): 

2782 

2783 ''' 

2784 A layout manager containing a center widget sorrounded by four margin 

2785 widgets. 

2786 

2787 :: 

2788 

2789 +---------------------------+ 

2790 | top | 

2791 +---------------------------+ 

2792 | | | | 

2793 | left | center | right | 

2794 | | | | 

2795 +---------------------------+ 

2796 | bottom | 

2797 +---------------------------+ 

2798 

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 ''' 

2810 

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) 

2827 

2828 def set_fixed_margins(self, left, right, top, bottom): 

2829 ''' 

2830 Give margins fixed size constraints. 

2831 ''' 

2832 

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) 

2837 

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. 

2841 

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) 

2848 

2849 def get_min_size(self): 

2850 shs, svs = Widget.get_min_size(self) 

2851 

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)] 

2856 

2857 shsum = sl[0]+sr[0]+sc[0] 

2858 svsum = st[1]+sb[1]+sc[1] 

2859 

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 

2864 

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 

2868 

2869 sh = max(shs, shsum) 

2870 sv = max(svs, svsum) 

2871 

2872 return sh, sv 

2873 

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 

2883 

2884 def set_size(self, size, offset): 

2885 (sh, sv), (oh, ov) = self.legalize(size, offset) 

2886 

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)] 

2891 

2892 ah = sh - (sl[0]+sr[0]+sc[0]) 

2893 av = sv - (st[1]+sb[1]+sc[1]) 

2894 

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)) 

2903 

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) 

2908 

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) 

2916 

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) 

2921 

2922 ah = sh - (slh+srh+sch) 

2923 av = sv - (stv+sbv+scv) 

2924 

2925 oh += ah/2. 

2926 ov += av/2. 

2927 sh -= ah 

2928 sv -= av 

2929 

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)) 

2936 

2937 def set_widget(self, which='center', widget=None): 

2938 

2939 ''' 

2940 Set one of the sub-widgets. 

2941 

2942 ``which`` should be one of ``'left'``, ``'right'``, ``'top'``, 

2943 ``'bottom'`` or ``'center'``. 

2944 ''' 

2945 

2946 if widget is None: 

2947 widget = Widget() 

2948 

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) 

2953 

2954 widget.set_parent(self) 

2955 

2956 def get_widget(self, which='center'): 

2957 

2958 ''' 

2959 Get one of the sub-widgets. 

2960 

2961 ``which`` should be one of ``'left'``, ``'right'``, ``'top'``, 

2962 ``'bottom'`` or ``'center'``. 

2963 ''' 

2964 

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) 

2969 

2970 def get_children(self): 

2971 return [self.left, self.right, self.top, self.bottom, self.center] 

2972 

2973 

2974class GridLayout(Widget): 

2975 

2976 ''' 

2977 A layout manager which arranges its sub-widgets in a grid. 

2978 

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. 

2982 

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 ''' 

2988 

2989 def __init__(self, nx=2, ny=2, horizontal=None, vertical=None): 

2990 

2991 ''' 

2992 Create new grid layout with ``nx`` columns and ``ny`` rows. 

2993 ''' 

2994 

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) 

3002 

3003 self.grid.append(row) 

3004 

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 

3013 

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 

3022 

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 

3031 

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 

3044 

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() 

3049 

3050 # available additional space 

3051 empty = esh.size == 0 

3052 

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 

3059 

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)) 

3068 

3069 nx, ny = esh.shape 

3070 

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) 

3077 

3078 nsh = num.maximum(nesh.max(0), esh.max(0)) 

3079 

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)) 

3084 

3085 ah = sh - sum(nsh) 

3086 av = sv - sum(nsv) 

3087 

3088 oh += ah/2. 

3089 ov += av/2. 

3090 sh -= ah 

3091 sv -= av 

3092 

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 

3101 

3102 Widget.set_size(self, (sh, sv), (oh, ov)) 

3103 

3104 def set_widget(self, ix, iy, widget=None): 

3105 

3106 ''' 

3107 Set one of the sub-widgets. 

3108 

3109 Sets the sub-widget in column ``ix`` and row ``iy``. The indices are 

3110 counted from zero. 

3111 ''' 

3112 

3113 if widget is None: 

3114 widget = Widget() 

3115 

3116 self.grid[iy][ix] = widget 

3117 widget.set_parent(self) 

3118 

3119 def get_widget(self, ix, iy): 

3120 

3121 ''' 

3122 Get one of the sub-widgets. 

3123 

3124 Gets the sub-widget from column ``ix`` and row ``iy``. The indices are 

3125 counted from zero. 

3126 ''' 

3127 

3128 return self.grid[iy][ix] 

3129 

3130 def get_children(self): 

3131 children = [] 

3132 for row in self.grid: 

3133 children.extend(row) 

3134 

3135 return children 

3136 

3137 

3138def is_gmt5(version='newest'): 

3139 return get_gmt_installation(version)['version'][0] in ['5', '6'] 

3140 

3141 

3142def is_gmt6(version='newest'): 

3143 return get_gmt_installation(version)['version'][0] in ['6'] 

3144 

3145 

3146def aspect_for_projection(gmtversion, *args, **kwargs): 

3147 

3148 gmt = GMT(version=gmtversion, eps_mode=True) 

3149 

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() 

3156 

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() 

3161 

3162 return (t-b)/(r-l) 

3163 

3164 

3165def text_box( 

3166 text, font=0, font_size=12., angle=0, gmtversion='newest', **kwargs): 

3167 

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 = [] 

3175 

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) 

3184 

3185 fn = gmt.tempfilename() + '.ps' 

3186 gmt.save(fn) 

3187 

3188 (_, stderr) = subprocess.Popen( 

3189 ['gs', '-q', '-dNOPAUSE', '-dBATCH', '-r720', '-sDEVICE=bbox', fn], 

3190 stderr=subprocess.PIPE).communicate() 

3191 

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 

3198 

3199 return dx, dy 

3200 

3201 

3202class TableLiner(object): 

3203 ''' 

3204 Utility class to turn tables into lines. 

3205 ''' 

3206 

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 

3211 

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) 

3217 

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) 

3222 

3223 

3224class LineStreamChopper(object): 

3225 ''' 

3226 File-like object to buffer data. 

3227 ''' 

3228 

3229 def __init__(self, liner): 

3230 self.chopsize = None 

3231 self.liner = liner 

3232 self.chop_iterator = None 

3233 self.closed = False 

3234 

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) 

3244 

3245 newbuf = BytesIO() 

3246 newbuf.write(buf.read()) 

3247 buf.close() 

3248 buf = newbuf 

3249 

3250 yield buf.getvalue() 

3251 buf.close() 

3252 

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() 

3259 

3260 self.chopsize = size 

3261 try: 

3262 return next(self.chop_iterator) 

3263 except StopIteration: 

3264 return '' 

3265 

3266 def close(self): 

3267 self.chopsize = None 

3268 self.chop_iterator = None 

3269 self.closed = True 

3270 

3271 def flush(self): 

3272 pass 

3273 

3274 

3275font_tab = { 

3276 0: 'Helvetica', 

3277 1: 'Helvetica-Bold', 

3278} 

3279 

3280font_tab_rev = dict((v, k) for (k, v) in font_tab.items()) 

3281 

3282 

3283class GMT(object): 

3284 ''' 

3285 A thin wrapper to GMT command execution. 

3286 

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. 

3292 

3293 Each instance of this class is used for the task of producing one PS or PDF 

3294 output file. 

3295 

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. 

3298 

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. 

3302 

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. 

3307 

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. 

3314 

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. 

3319 

3320 ''' 

3321 

3322 def __init__( 

3323 self, 

3324 config=None, 

3325 kontinue=None, 

3326 version='newest', 

3327 config_papersize=None, 

3328 eps_mode=False): 

3329 

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 

3334 

3335 if config: 

3336 self.gmt_config.update(config) 

3337 

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])) 

3342 

3343 if self.is_gmt5(): 

3344 self.gmt_config['PS_MEDIA'] = config_papersize 

3345 else: 

3346 self.gmt_config['PAPER_MEDIA'] = config_papersize 

3347 

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) 

3351 

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 

3358 

3359 self.finished = False 

3360 

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 

3365 

3366 self.layout = None 

3367 self.command_log = [] 

3368 self.keep_temp_dir = False 

3369 

3370 def is_gmt5(self): 

3371 return self.get_version()[0] in ['5', '6'] 

3372 

3373 def is_gmt6(self): 

3374 return self.get_version()[0] in ['6'] 

3375 

3376 def get_version(self): 

3377 return self.installation['version'] 

3378 

3379 def get_config(self, key): 

3380 return self.gmt_config[key] 

3381 

3382 def to_points(self, string): 

3383 if not string: 

3384 return 0 

3385 

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] 

3392 

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']) 

3398 

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'] 

3404 

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')) 

3410 

3411 for k, v in config.items(): 

3412 f.write(('%s = %s\n' % (k, v)).encode('ascii')) 

3413 f.close() 

3414 

3415 def __del__(self): 

3416 if not self.keep_temp_dir: 

3417 self._shutil.rmtree(self.tempdir) 

3418 

3419 def _gmtcommand(self, command, *addargs, **kwargs): 

3420 

3421 ''' 

3422 Execute arbitrary GMT command. 

3423 

3424 See docstring in __getattr__ for details. 

3425 ''' 

3426 

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) 

3438 

3439 assert not self.finished 

3440 

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])) 

3448 

3449 options = [] 

3450 

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 

3462 

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)) 

3467 

3468 if out_discard: 

3469 out_filename = '/dev/null' 

3470 

3471 out_mustclose = False 

3472 if out_filename is not None: 

3473 out_mustclose = True 

3474 out_stream = open(out_filename, 'wb') 

3475 

3476 if in_filename is not None: 

3477 in_stream = open(in_filename, 'rb') 

3478 

3479 if in_string is not None: 

3480 in_stream = BytesIO(in_string) 

3481 

3482 encoding_gmt = gmt_config.get( 

3483 'PS_CHAR_ENCODING', 

3484 gmt_config.get('CHAR_ENCODING', 'ISOLatin1+')) 

3485 

3486 encoding = encoding_gmt_to_python[encoding_gmt.lower()] 

3487 

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)) 

3492 

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)) 

3499 

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))) 

3507 

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 

3514 

3515 if not self.needstart: 

3516 options.append('-O') 

3517 else: 

3518 self.needstart = False 

3519 

3520 out_stream = self.output 

3521 

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)] 

3527 

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) 

3535 

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 

3554 

3555 p.stdin.close() 

3556 

3557 while True: 

3558 data = p.stdout.read(bs) 

3559 if len(data) == 0: 

3560 break 

3561 out_stream.write(data) 

3562 

3563 p.stdout.close() 

3564 

3565 retcode = p.wait() 

3566 

3567 if in_stream is not None: 

3568 in_stream.close() 

3569 

3570 if out_mustclose: 

3571 out_stream.close() 

3572 

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))) 

3578 

3579 self.command_log.append(args) 

3580 

3581 def __getattr__(self, command): 

3582 

3583 ''' 

3584 Maps to call self._gmtcommand(command, \\*addargs, \\*\\*kwargs). 

3585 

3586 Execute arbitrary GMT command. 

3587 

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. 

3591 

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. 

3600 

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. 

3605 

3606 The standard input of the GMT process is fed by data selected with one 

3607 of the following ``in_*`` keyword arguments: 

3608 

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 =============== ======================================================= 

3621 

3622 The standard output of the GMT process may be redirected by one of the 

3623 following options: 

3624 

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 ================= ===================================================== 

3630 

3631 Additional keyword arguments: 

3632 

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 ===================== ================================================= 

3643 

3644 ''' 

3645 

3646 def f(*args, **kwargs): 

3647 return self._gmtcommand(command, *args, **kwargs) 

3648 return f 

3649 

3650 def tempfilename(self, name=None): 

3651 ''' 

3652 Get filename for temporary file in the private temp directory. 

3653 

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 ''' 

3657 

3658 if not name: 

3659 name = ''.join( 

3660 [random.choice('abcdefghijklmnopqrstuvwxyz') 

3661 for i in range(10)]) 

3662 

3663 fn = pjoin(self.tempdir, name) 

3664 return fn 

3665 

3666 def tempfile(self, name=None): 

3667 ''' 

3668 Create and open a file in the private temp directory. 

3669 ''' 

3670 

3671 fn = self.tempfilename(name) 

3672 f = open(fn, 'wb') 

3673 return f, fn 

3674 

3675 def save_unfinished(self, filename): 

3676 out = open(filename, 'wb') 

3677 out.write(self.output.getvalue()) 

3678 out.close() 

3679 

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() 

3686 

3687 def dump(self, ident): 

3688 filename = self.tempfilename('breakpoint-%s' % ident) 

3689 self.save_unfinished(filename) 

3690 

3691 def load(self, ident): 

3692 filename = self.tempfilename('breakpoint-%s' % ident) 

3693 self.load_unfinished(filename) 

3694 

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): 

3698 

3699 ''' 

3700 Finish and save figure as PDF, PS or PPM file. 

3701 

3702 If filename ends with ``'.pdf'`` a PDF file is created by piping the 

3703 GMT output through :program:`gmtpy-epstopdf`. 

3704 

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``. 

3713 

3714 The bounding box is set according to the values given in ``bbox``. 

3715 ''' 

3716 

3717 if not self.finished: 

3718 self.psxy(R=True, J=True, finish=True) 

3719 

3720 if filename: 

3721 tempfn = pjoin(self.tempdir, 'incomplete') 

3722 out = open(tempfn, 'wb') 

3723 else: 

3724 out = sys.stdout 

3725 

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()) 

3730 

3731 if filename: 

3732 out.close() 

3733 

3734 if filename.endswith('.ps') or ( 

3735 not self.is_gmt5() and filename.endswith('.eps')): 

3736 

3737 shutil.move(tempfn, filename) 

3738 return 

3739 

3740 if self.is_gmt5(): 

3741 if crop_eps_mode: 

3742 addarg = ['-A'] 

3743 else: 

3744 addarg = [] 

3745 

3746 subprocess.call( 

3747 [pjoin(self.installation['bin'], 'gmt'), 'psconvert', 

3748 '-Te', '-F%s' % tempfn, tempfn, ] + addarg) 

3749 

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) 

3754 

3755 shutil.move(tempfn + '-fixbb.eps', tempfn + '.eps') 

3756 

3757 else: 

3758 shutil.move(tempfn, tempfn + '.eps') 

3759 

3760 if filename.endswith('.eps'): 

3761 shutil.move(tempfn + '.eps', filename) 

3762 return 

3763 

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']) 

3777 

3778 convert_graph( 

3779 tempfn + '.pdf', filename, 

3780 resolution=resolution, oversample=oversample, 

3781 size=size, width=width, height=height) 

3782 

3783 def bbox(self): 

3784 return get_bbox(self.output.getvalue()) 

3785 

3786 def get_command_log(self): 

3787 ''' 

3788 Get the command log. 

3789 ''' 

3790 

3791 return self.command_log 

3792 

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 

3798 

3799 def page_size_points(self): 

3800 ''' 

3801 Try to get paper size of output postscript file in points. 

3802 ''' 

3803 

3804 pm = paper_media(self.gmt_config).lower() 

3805 if pm.endswith('+') or pm.endswith('-'): 

3806 pm = pm[:-1] 

3807 

3808 orient = page_orientation(self.gmt_config).lower() 

3809 

3810 if pm in all_paper_sizes(): 

3811 

3812 if orient == 'portrait': 

3813 return get_paper_size(pm) 

3814 else: 

3815 return get_paper_size(pm)[1], get_paper_size(pm)[0] 

3816 

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 

3829 

3830 return None, None 

3831 

3832 def default_layout(self, with_palette=False): 

3833 ''' 

3834 Get a default layout for the output page. 

3835 

3836 One of three different layouts is choosen, depending on the 

3837 `PAPER_MEDIA` setting in the GMT configuration dict. 

3838 

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`. 

3843 

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`. 

3848 

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. 

3854 

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 ''' 

3859 

3860 if self.layout is None: 

3861 w, h = self.page_size_points() 

3862 

3863 if w is None or h is None: 

3864 raise GmtPyError("Can't determine page size for layout") 

3865 

3866 pm = paper_media(self.gmt_config).lower() 

3867 

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) 

3874 

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 

3891 

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) 

3905 

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') 

3915 

3916 widget.set_aspect(1./golden_ratio) 

3917 

3918 self.layout = inner 

3919 

3920 return self.layout 

3921 

3922 def draw_layout(self, layout): 

3923 ''' 

3924 Use psxy to draw layout; for debugging 

3925 ''' 

3926 

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) 

3936 

3937 # points = num.array(corners, dtype=float) 

3938 

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) 

3945 

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()) 

3954 

3955 

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'): 

3963 

3964 if x+k in conf: 

3965 c[k] = conf[x+k] 

3966 

3967 return Ax(**c) 

3968 

3969 

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 

3981 

3982 

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'): 

3992 

3993 self.data = data 

3994 self.size = size 

3995 self.justify = justify 

3996 self.fontno = fontno 

3997 self.offset = offset 

3998 self.color = color 

3999 

4000 

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 = [] 

4009 

4010 self.gmtversion = gmtversion 

4011 

4012 self.data_x = [] 

4013 self.symbols_x = [] 

4014 

4015 self.data_y = [] 

4016 self.symbols_y = [] 

4017 

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) 

4028 

4029 self.setup_defaults() 

4030 self.fixate_widget_aspect = False 

4031 

4032 def setup_defaults(self): 

4033 pass 

4034 

4035 def set_defaults(self, **kwargs): 

4036 self.default_config.update(kwargs) 

4037 

4038 def plot(self, data, symbol=''): 

4039 self.data.append(data) 

4040 self.symbols.append(symbol) 

4041 

4042 def density_plot(self, data, **kwargs): 

4043 dpd = DensityPlotDef(data, **kwargs) 

4044 self.density_plot_defs.append(dpd) 

4045 

4046 def text(self, data, **kwargs): 

4047 dpd = TextDef(data, **kwargs) 

4048 self.text_defs.append(dpd) 

4049 

4050 def plot_x(self, data, symbol=''): 

4051 self.data_x.append(data) 

4052 self.symbols_x.append(symbol) 

4053 

4054 def plot_y(self, data, symbol=''): 

4055 self.data_y.append(data) 

4056 self.symbols_y.append(symbol) 

4057 

4058 def set(self, **kwargs): 

4059 self.config.update(kwargs) 

4060 

4061 def setup_base(self, conf): 

4062 w = conf.pop('width') 

4063 h = conf.pop('height') 

4064 margins = conf.pop('margins') 

4065 

4066 gmtconfig = {} 

4067 if self.gmtconfig is not None: 

4068 gmtconfig.update(self.gmtconfig) 

4069 

4070 gmt = GMT( 

4071 version=self.gmtversion, 

4072 config=gmtconfig, 

4073 config_papersize='Custom_%ix%i' % (w, h)) 

4074 

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 

4091 

4092 def setup_projection(self, widget, scaler, conf): 

4093 pass 

4094 

4095 def setup_scaling(self, conf): 

4096 ndims = 2 

4097 if self.density_plot_defs: 

4098 ndims = 3 

4099 

4100 axes = [simpleconf_to_ax(conf, x) for x in 'xyz'[:ndims]] 

4101 

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] 

4110 

4111 scaler = ScaleGuru(data_chopped, axes=axes[:ndims]) 

4112 

4113 self.setup_scaling_plus(scaler, axes[:ndims]) 

4114 

4115 return scaler 

4116 

4117 def setup_scaling_plus(self, scaler, axes): 

4118 pass 

4119 

4120 def setup_scaling_extra(self, scaler, conf): 

4121 

4122 scaler_x = scaler.copy() 

4123 scaler_x.data_ranges[1] = (0., 1.) 

4124 scaler_x.axes[1].mode = 'off' 

4125 

4126 scaler_y = scaler.copy() 

4127 scaler_y.data_ranges[0] = (0., 1.) 

4128 scaler_y.axes[0].mode = 'off' 

4129 

4130 return scaler_x, scaler_y 

4131 

4132 def draw_density(self, gmt, widget, scaler): 

4133 

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: 

4139 

4140 fn_cpt = gmt.tempfilename() + '.cpt' 

4141 

4142 if dpd.zscaler is not None: 

4143 s = dpd.zscaler 

4144 else: 

4145 s = scaler 

4146 

4147 gmt.makecpt(C=dpd.cpt, out_filename=fn_cpt, *s.T()) 

4148 

4149 fn_grid = gmt.tempfilename() 

4150 

4151 fn_mean = gmt.tempfilename() 

4152 

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) 

4157 

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) 

4166 

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) 

4175 

4176 if gmt.is_gmt5(): 

4177 gmt.grdimage(fn_grid, C=fn_cpt, E='i', n='l', *rxyj) 

4178 

4179 else: 

4180 gmt.grdimage(fn_grid, C=fn_cpt, E='i', S='l', *rxyj) 

4181 

4182 if dpd.contour: 

4183 gmt.grdcontour(fn_grid, C=fn_cpt, W='0.5p,black', *rxyj) 

4184 innerticks = '0.5p,black' 

4185 

4186 os.remove(fn_grid) 

4187 os.remove(fn_mean) 

4188 

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 

4194 

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) 

4199 

4200 return fn_cpt, innerticks 

4201 

4202 def draw_basemap(self, gmt, widget, scaler): 

4203 gmt.psbasemap(*(widget.JXY() + scaler.RB(ax_projection=True))) 

4204 

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)) 

4209 

4210 def post_draw(self, gmt, widget, scaler): 

4211 pass 

4212 

4213 def pre_draw(self, gmt, widget, scaler): 

4214 pass 

4215 

4216 def draw_extra(self, gmt, widget, scaler_x, scaler_y): 

4217 

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())) 

4221 

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())) 

4225 

4226 def draw_text(self, gmt, widget, scaler): 

4227 

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) 

4247 

4248 def save(self, filename, resolution=150): 

4249 

4250 conf = dict(self.default_config) 

4251 conf.update(self.config) 

4252 

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) 

4256 

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())) 

4261 

4262 widget.set_aspect(aspect) 

4263 

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) 

4275 

4276 if palette_widget and cptfile: 

4277 nice_palette(gmt, palette_widget, scaler, cptfile, 

4278 innerticks=innerticks, 

4279 zlabeloffset=conf['zlabeloffset']) 

4280 

4281 gmt.save(filename, resolution=resolution) 

4282 

4283 

4284class LinLinPlot(Simple): 

4285 pass 

4286 

4287 

4288class LogLinPlot(Simple): 

4289 

4290 def setup_defaults(self): 

4291 self.set_defaults(xmode='min-max') 

4292 

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' 

4296 

4297 

4298class LinLogPlot(Simple): 

4299 

4300 def setup_defaults(self): 

4301 self.set_defaults(ymode='min-max') 

4302 

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' 

4306 

4307 

4308class LogLogPlot(Simple): 

4309 

4310 def setup_defaults(self): 

4311 self.set_defaults(mode='min-max') 

4312 

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' 

4316 

4317 

4318class AziDistPlot(Simple): 

4319 

4320 def __init__(self, *args, **kwargs): 

4321 Simple.__init__(self, *args, **kwargs) 

4322 self.fixate_widget_aspect = True 

4323 

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.) 

4331 

4332 def setup_projection(self, widget, scaler, conf): 

4333 widget['J'] = '-JPa%(width)gp' 

4334 

4335 def setup_scaling_plus(self, scaler, axes): 

4336 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/%(yinc)g:%(ylabel)s:N' 

4337 

4338 

4339class MPlot(Simple): 

4340 

4341 def __init__(self, *args, **kwargs): 

4342 Simple.__init__(self, *args, **kwargs) 

4343 self.fixate_widget_aspect = True 

4344 

4345 def setup_defaults(self): 

4346 self.set_defaults(xmode='min-max', ymode='min-max') 

4347 

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' 

4356 

4357 

4358def nice_palette(gmt, widget, scaleguru, cptfile, zlabeloffset=0.8*inch, 

4359 innerticks=True): 

4360 

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) 

4375 

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()) 

4381 

4382 negpalwid = '%gp' % -widget.width() 

4383 if not isinstance(innerticks, str) and innerticks: 

4384 ticklen = negpalwid 

4385 else: 

4386 ticklen = '0p' 

4387 

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()) 

4393 

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()) 

4401 

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())