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 

30from scipy.io import netcdf 

31 

32from pyrocko import ExternalProgramMissing 

33 

34try: 

35 newstr = unicode 

36except NameError: 

37 newstr = str 

38 

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

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

41 

42 

43encoding_gmt_to_python = { 

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

45 'standard+': 'ascii', 

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

47 'standard': 'ascii'} 

48 

49for i in range(1, 11): 

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

51 

52 

53def have_gmt(): 

54 try: 

55 get_gmt_installation('newest') 

56 return True 

57 

58 except GMTInstallationProblem: 

59 return False 

60 

61 

62def check_have_gmt(): 

63 if not have_gmt(): 

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

65 

66 

67def have_pixmaptools(): 

68 for prog in [['pdftocairo'], ['convert'], ['gs', '-h']]: 

69 try: 

70 p = subprocess.Popen( 

71 prog, 

72 stdout=subprocess.PIPE, stderr=subprocess.PIPE) 

73 

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

75 

76 except OSError: 

77 return False 

78 

79 return True 

80 

81 

82class GmtPyError(Exception): 

83 pass 

84 

85 

86class GMTError(GmtPyError): 

87 pass 

88 

89 

90class GMTInstallationProblem(GmtPyError): 

91 pass 

92 

93 

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

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

96 

97 _, tmp_filename_base = tempfile.mkstemp() 

98 

99 try: 

100 if out_filename.endswith('.svg'): 

101 fmt_arg = '-svg' 

102 tmp_filename = tmp_filename_base 

103 oversample = 1.0 

104 else: 

105 fmt_arg = '-png' 

106 tmp_filename = tmp_filename_base + '-1.png' 

107 

108 if size is not None: 

109 scale_args = ['-scale-to', '%i' % int(round(size*oversample))] 

110 elif width is not None: 

111 scale_args = ['-scale-to-x', '%i' % int(round(width*oversample))] 

112 elif height is not None: 

113 scale_args = ['-scale-to-y', '%i' % int(round(height*oversample))] 

114 else: 

115 scale_args = ['-r', '%i' % int(round(resolution * oversample))] 

116 

117 try: 

118 subprocess.check_call( 

119 ['pdftocairo'] + scale_args + 

120 [fmt_arg, in_filename, tmp_filename_base]) 

121 except OSError as e: 

122 raise GmtPyError( 

123 'Cannot start `pdftocairo`, is it installed? (%s)' % str(e)) 

124 

125 if oversample > 1.: 

126 try: 

127 subprocess.check_call([ 

128 'convert', 

129 tmp_filename, 

130 '-resize', '%i%%' % int(round(100.0/oversample)), 

131 out_filename]) 

132 except OSError as e: 

133 raise GmtPyError( 

134 'Cannot start `convert`, is it installed? (%s)' % str(e)) 

135 

136 else: 

137 if out_filename.endswith('.png') or out_filename.endswith('.svg'): 

138 shutil.move(tmp_filename, out_filename) 

139 else: 

140 try: 

141 subprocess.check_call( 

142 ['convert', tmp_filename, out_filename]) 

143 except Exception as e: 

144 raise GmtPyError( 

145 'Cannot start `convert`, is it installed? (%s)' 

146 % str(e)) 

147 

148 except Exception: 

149 raise 

150 

151 finally: 

152 if os.path.exists(tmp_filename_base): 

153 os.remove(tmp_filename_base) 

154 

155 if os.path.exists(tmp_filename): 

156 os.remove(tmp_filename) 

157 

158 

159def get_bbox(s): 

160 for pat in [find_hiresbb, find_bb]: 

161 m = pat.search(s) 

162 if m: 

163 bb = [float(x) for x in m.group(1).split()] 

164 return bb 

165 

166 raise GmtPyError('Cannot find bbox') 

167 

168 

169def replace_bbox(bbox, *args): 

170 

171 def repl(m): 

172 if m.group(1): 

173 return ('%%HiResBoundingBox: ' + ' '.join( 

174 '%.3f' % float(x) for x in bbox)).encode('ascii') 

175 else: 

176 return ('%%%%BoundingBox: %i %i %i %i' % ( 

177 int(math.floor(bbox[0])), 

178 int(math.floor(bbox[1])), 

179 int(math.ceil(bbox[2])), 

180 int(math.ceil(bbox[3])))).encode('ascii') 

181 

182 pat = re.compile(br'%%(HiRes)?BoundingBox:((\s+[0-9.]+){4})') 

183 if len(args) == 1: 

184 s = args[0] 

185 return pat.sub(repl, s) 

186 

187 else: 

188 fin, fout = args 

189 nn = 0 

190 for line in fin: 

191 line, n = pat.subn(repl, line) 

192 nn += n 

193 fout.write(line) 

194 if nn == 2: 

195 break 

196 

197 if nn == 2: 

198 for line in fin: 

199 fout.write(line) 

200 

201 

202def escape_shell_arg(s): 

203 ''' 

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

205 insecure. 

206 ''' 

207 

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

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

210 else: 

211 return s 

212 

213 

214def escape_shell_args(args): 

215 ''' 

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

217 insecure. 

218 ''' 

219 

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

221 

222 

223golden_ratio = 1.61803 

224 

225# units in points 

226_units = { 

227 'i': 72., 

228 'c': 72./2.54, 

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

230 'p': 1.} 

231 

232inch = _units['i'] 

233cm = _units['c'] 

234 

235# some awsome colors 

236tango_colors = { 

237 'butter1': (252, 233, 79), 

238 'butter2': (237, 212, 0), 

239 'butter3': (196, 160, 0), 

240 'chameleon1': (138, 226, 52), 

241 'chameleon2': (115, 210, 22), 

242 'chameleon3': (78, 154, 6), 

243 'orange1': (252, 175, 62), 

244 'orange2': (245, 121, 0), 

245 'orange3': (206, 92, 0), 

246 'skyblue1': (114, 159, 207), 

247 'skyblue2': (52, 101, 164), 

248 'skyblue3': (32, 74, 135), 

249 'plum1': (173, 127, 168), 

250 'plum2': (117, 80, 123), 

251 'plum3': (92, 53, 102), 

252 'chocolate1': (233, 185, 110), 

253 'chocolate2': (193, 125, 17), 

254 'chocolate3': (143, 89, 2), 

255 'scarletred1': (239, 41, 41), 

256 'scarletred2': (204, 0, 0), 

257 'scarletred3': (164, 0, 0), 

258 'aluminium1': (238, 238, 236), 

259 'aluminium2': (211, 215, 207), 

260 'aluminium3': (186, 189, 182), 

261 'aluminium4': (136, 138, 133), 

262 'aluminium5': (85, 87, 83), 

263 'aluminium6': (46, 52, 54) 

264} 

265 

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

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

268 'butter2')] 

269 

270 

271def color(x=None): 

272 ''' 

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

274 

275 If ``x`` is None, a random color is returned. If it is an integer, the 

276 corresponding ``gmtpy.graph_colors[x]`` or black returned. If it is a 

277 string and the corresponding ``gmtpy.tango_colors[x]`` exists, this is 

278 returned, or the string is passed through. If ``x`` is a tuple, it is 

279 transformed into the string form which GMT expects. 

280 ''' 

281 

282 if x is None: 

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

284 

285 if isinstance(x, int): 

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

287 return '%i/%i/%i' % graph_colors[x] 

288 else: 

289 return '0/0/0' 

290 

291 elif isinstance(x, str): 

292 if x in tango_colors: 

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

294 else: 

295 return x 

296 

297 return '%i/%i/%i' % x 

298 

299 

300def color_tup(x=None): 

301 if x is None: 

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

303 

304 if isinstance(x, int): 

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

306 return graph_colors[x] 

307 else: 

308 return (0, 0, 0) 

309 

310 elif isinstance(x, str): 

311 if x in tango_colors: 

312 return tango_colors[x] 

313 

314 return x 

315 

316 

317_gmt_installations = {} 

318 

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

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

321 

322# _gmt_installations['4.2.1'] = {'home': '/sw/etch-ia32/gmt-4.2.1', 

323# 'bin': '/sw/etch-ia32/gmt-4.2.1/bin'} 

324# _gmt_installations['4.3.0'] = {'home': '/sw/etch-ia32/gmt-4.3.0', 

325# 'bin': '/sw/etch-ia32/gmt-4.3.0/bin'} 

326# _gmt_installations['6.0.0'] = {'home': '/usr/share/gmt', 

327# 'bin': '/usr/bin' } 

328 

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

330 

331 

332def key_version(a): 

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

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

335 

336 

337def newest_installed_gmt_version(): 

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

339 

340 

341def all_installed_gmt_versions(): 

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

343 

344 

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

346# changed. 

347 

348_gmt_defaults_by_version = {} 

349_gmt_defaults_by_version['4.2.1'] = r''' 

350# 

351# GMT-SYSTEM 4.2.1 Defaults file 

352# 

353#-------- Plot Media Parameters ------------- 

354PAGE_COLOR = 255/255/255 

355PAGE_ORIENTATION = portrait 

356PAPER_MEDIA = a4+ 

357#-------- Basemap Annotation Parameters ------ 

358ANNOT_MIN_ANGLE = 20 

359ANNOT_MIN_SPACING = 0 

360ANNOT_FONT_PRIMARY = Helvetica 

361ANNOT_FONT_SIZE = 12p 

362ANNOT_OFFSET_PRIMARY = 0.075i 

363ANNOT_FONT_SECONDARY = Helvetica 

364ANNOT_FONT_SIZE_SECONDARY = 16p 

365ANNOT_OFFSET_SECONDARY = 0.075i 

366DEGREE_SYMBOL = ring 

367HEADER_FONT = Helvetica 

368HEADER_FONT_SIZE = 36p 

369HEADER_OFFSET = 0.1875i 

370LABEL_FONT = Helvetica 

371LABEL_FONT_SIZE = 14p 

372LABEL_OFFSET = 0.1125i 

373OBLIQUE_ANNOTATION = 1 

374PLOT_CLOCK_FORMAT = hh:mm:ss 

375PLOT_DATE_FORMAT = yyyy-mm-dd 

376PLOT_DEGREE_FORMAT = +ddd:mm:ss 

377Y_AXIS_TYPE = hor_text 

378#-------- Basemap Layout Parameters --------- 

379BASEMAP_AXES = WESN 

380BASEMAP_FRAME_RGB = 0/0/0 

381BASEMAP_TYPE = plain 

382FRAME_PEN = 1.25p 

383FRAME_WIDTH = 0.075i 

384GRID_CROSS_SIZE_PRIMARY = 0i 

385GRID_CROSS_SIZE_SECONDARY = 0i 

386GRID_PEN_PRIMARY = 0.25p 

387GRID_PEN_SECONDARY = 0.5p 

388MAP_SCALE_HEIGHT = 0.075i 

389TICK_LENGTH = 0.075i 

390POLAR_CAP = 85/90 

391TICK_PEN = 0.5p 

392X_AXIS_LENGTH = 9i 

393Y_AXIS_LENGTH = 6i 

394X_ORIGIN = 1i 

395Y_ORIGIN = 1i 

396UNIX_TIME = FALSE 

397UNIX_TIME_POS = -0.75i/-0.75i 

398#-------- Color System Parameters ----------- 

399COLOR_BACKGROUND = 0/0/0 

400COLOR_FOREGROUND = 255/255/255 

401COLOR_NAN = 128/128/128 

402COLOR_IMAGE = adobe 

403COLOR_MODEL = rgb 

404HSV_MIN_SATURATION = 1 

405HSV_MAX_SATURATION = 0.1 

406HSV_MIN_VALUE = 0.3 

407HSV_MAX_VALUE = 1 

408#-------- PostScript Parameters ------------- 

409CHAR_ENCODING = ISOLatin1+ 

410DOTS_PR_INCH = 300 

411N_COPIES = 1 

412PS_COLOR = rgb 

413PS_IMAGE_COMPRESS = none 

414PS_IMAGE_FORMAT = ascii 

415PS_LINE_CAP = round 

416PS_LINE_JOIN = miter 

417PS_MITER_LIMIT = 35 

418PS_VERBOSE = FALSE 

419GLOBAL_X_SCALE = 1 

420GLOBAL_Y_SCALE = 1 

421#-------- I/O Format Parameters ------------- 

422D_FORMAT = %lg 

423FIELD_DELIMITER = tab 

424GRIDFILE_SHORTHAND = FALSE 

425GRID_FORMAT = nf 

426INPUT_CLOCK_FORMAT = hh:mm:ss 

427INPUT_DATE_FORMAT = yyyy-mm-dd 

428IO_HEADER = FALSE 

429N_HEADER_RECS = 1 

430OUTPUT_CLOCK_FORMAT = hh:mm:ss 

431OUTPUT_DATE_FORMAT = yyyy-mm-dd 

432OUTPUT_DEGREE_FORMAT = +D 

433XY_TOGGLE = FALSE 

434#-------- Projection Parameters ------------- 

435ELLIPSOID = WGS-84 

436MAP_SCALE_FACTOR = default 

437MEASURE_UNIT = inch 

438#-------- Calendar/Time Parameters ---------- 

439TIME_FORMAT_PRIMARY = full 

440TIME_FORMAT_SECONDARY = full 

441TIME_EPOCH = 2000-01-01T00:00:00 

442TIME_IS_INTERVAL = OFF 

443TIME_INTERVAL_FRACTION = 0.5 

444TIME_LANGUAGE = us 

445TIME_SYSTEM = other 

446TIME_UNIT = d 

447TIME_WEEK_START = Sunday 

448Y2K_OFFSET_YEAR = 1950 

449#-------- Miscellaneous Parameters ---------- 

450HISTORY = TRUE 

451INTERPOLANT = akima 

452LINE_STEP = 0.01i 

453VECTOR_SHAPE = 0 

454VERBOSE = FALSE''' 

455 

456_gmt_defaults_by_version['4.3.0'] = r''' 

457# 

458# GMT-SYSTEM 4.3.0 Defaults file 

459# 

460#-------- Plot Media Parameters ------------- 

461PAGE_COLOR = 255/255/255 

462PAGE_ORIENTATION = portrait 

463PAPER_MEDIA = a4+ 

464#-------- Basemap Annotation Parameters ------ 

465ANNOT_MIN_ANGLE = 20 

466ANNOT_MIN_SPACING = 0 

467ANNOT_FONT_PRIMARY = Helvetica 

468ANNOT_FONT_SIZE_PRIMARY = 12p 

469ANNOT_OFFSET_PRIMARY = 0.075i 

470ANNOT_FONT_SECONDARY = Helvetica 

471ANNOT_FONT_SIZE_SECONDARY = 16p 

472ANNOT_OFFSET_SECONDARY = 0.075i 

473DEGREE_SYMBOL = ring 

474HEADER_FONT = Helvetica 

475HEADER_FONT_SIZE = 36p 

476HEADER_OFFSET = 0.1875i 

477LABEL_FONT = Helvetica 

478LABEL_FONT_SIZE = 14p 

479LABEL_OFFSET = 0.1125i 

480OBLIQUE_ANNOTATION = 1 

481PLOT_CLOCK_FORMAT = hh:mm:ss 

482PLOT_DATE_FORMAT = yyyy-mm-dd 

483PLOT_DEGREE_FORMAT = +ddd:mm:ss 

484Y_AXIS_TYPE = hor_text 

485#-------- Basemap Layout Parameters --------- 

486BASEMAP_AXES = WESN 

487BASEMAP_FRAME_RGB = 0/0/0 

488BASEMAP_TYPE = plain 

489FRAME_PEN = 1.25p 

490FRAME_WIDTH = 0.075i 

491GRID_CROSS_SIZE_PRIMARY = 0i 

492GRID_PEN_PRIMARY = 0.25p 

493GRID_CROSS_SIZE_SECONDARY = 0i 

494GRID_PEN_SECONDARY = 0.5p 

495MAP_SCALE_HEIGHT = 0.075i 

496POLAR_CAP = 85/90 

497TICK_LENGTH = 0.075i 

498TICK_PEN = 0.5p 

499X_AXIS_LENGTH = 9i 

500Y_AXIS_LENGTH = 6i 

501X_ORIGIN = 1i 

502Y_ORIGIN = 1i 

503UNIX_TIME = FALSE 

504UNIX_TIME_POS = BL/-0.75i/-0.75i 

505UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S 

506#-------- Color System Parameters ----------- 

507COLOR_BACKGROUND = 0/0/0 

508COLOR_FOREGROUND = 255/255/255 

509COLOR_NAN = 128/128/128 

510COLOR_IMAGE = adobe 

511COLOR_MODEL = rgb 

512HSV_MIN_SATURATION = 1 

513HSV_MAX_SATURATION = 0.1 

514HSV_MIN_VALUE = 0.3 

515HSV_MAX_VALUE = 1 

516#-------- PostScript Parameters ------------- 

517CHAR_ENCODING = ISOLatin1+ 

518DOTS_PR_INCH = 300 

519N_COPIES = 1 

520PS_COLOR = rgb 

521PS_IMAGE_COMPRESS = none 

522PS_IMAGE_FORMAT = ascii 

523PS_LINE_CAP = round 

524PS_LINE_JOIN = miter 

525PS_MITER_LIMIT = 35 

526PS_VERBOSE = FALSE 

527GLOBAL_X_SCALE = 1 

528GLOBAL_Y_SCALE = 1 

529#-------- I/O Format Parameters ------------- 

530D_FORMAT = %lg 

531FIELD_DELIMITER = tab 

532GRIDFILE_SHORTHAND = FALSE 

533GRID_FORMAT = nf 

534INPUT_CLOCK_FORMAT = hh:mm:ss 

535INPUT_DATE_FORMAT = yyyy-mm-dd 

536IO_HEADER = FALSE 

537N_HEADER_RECS = 1 

538OUTPUT_CLOCK_FORMAT = hh:mm:ss 

539OUTPUT_DATE_FORMAT = yyyy-mm-dd 

540OUTPUT_DEGREE_FORMAT = +D 

541XY_TOGGLE = FALSE 

542#-------- Projection Parameters ------------- 

543ELLIPSOID = WGS-84 

544MAP_SCALE_FACTOR = default 

545MEASURE_UNIT = inch 

546#-------- Calendar/Time Parameters ---------- 

547TIME_FORMAT_PRIMARY = full 

548TIME_FORMAT_SECONDARY = full 

549TIME_EPOCH = 2000-01-01T00:00:00 

550TIME_IS_INTERVAL = OFF 

551TIME_INTERVAL_FRACTION = 0.5 

552TIME_LANGUAGE = us 

553TIME_UNIT = d 

554TIME_WEEK_START = Sunday 

555Y2K_OFFSET_YEAR = 1950 

556#-------- Miscellaneous Parameters ---------- 

557HISTORY = TRUE 

558INTERPOLANT = akima 

559LINE_STEP = 0.01i 

560VECTOR_SHAPE = 0 

561VERBOSE = FALSE''' 

562 

563 

564_gmt_defaults_by_version['4.3.1'] = r''' 

565# 

566# GMT-SYSTEM 4.3.1 Defaults file 

567# 

568#-------- Plot Media Parameters ------------- 

569PAGE_COLOR = 255/255/255 

570PAGE_ORIENTATION = portrait 

571PAPER_MEDIA = a4+ 

572#-------- Basemap Annotation Parameters ------ 

573ANNOT_MIN_ANGLE = 20 

574ANNOT_MIN_SPACING = 0 

575ANNOT_FONT_PRIMARY = Helvetica 

576ANNOT_FONT_SIZE_PRIMARY = 12p 

577ANNOT_OFFSET_PRIMARY = 0.075i 

578ANNOT_FONT_SECONDARY = Helvetica 

579ANNOT_FONT_SIZE_SECONDARY = 16p 

580ANNOT_OFFSET_SECONDARY = 0.075i 

581DEGREE_SYMBOL = ring 

582HEADER_FONT = Helvetica 

583HEADER_FONT_SIZE = 36p 

584HEADER_OFFSET = 0.1875i 

585LABEL_FONT = Helvetica 

586LABEL_FONT_SIZE = 14p 

587LABEL_OFFSET = 0.1125i 

588OBLIQUE_ANNOTATION = 1 

589PLOT_CLOCK_FORMAT = hh:mm:ss 

590PLOT_DATE_FORMAT = yyyy-mm-dd 

591PLOT_DEGREE_FORMAT = +ddd:mm:ss 

592Y_AXIS_TYPE = hor_text 

593#-------- Basemap Layout Parameters --------- 

594BASEMAP_AXES = WESN 

595BASEMAP_FRAME_RGB = 0/0/0 

596BASEMAP_TYPE = plain 

597FRAME_PEN = 1.25p 

598FRAME_WIDTH = 0.075i 

599GRID_CROSS_SIZE_PRIMARY = 0i 

600GRID_PEN_PRIMARY = 0.25p 

601GRID_CROSS_SIZE_SECONDARY = 0i 

602GRID_PEN_SECONDARY = 0.5p 

603MAP_SCALE_HEIGHT = 0.075i 

604POLAR_CAP = 85/90 

605TICK_LENGTH = 0.075i 

606TICK_PEN = 0.5p 

607X_AXIS_LENGTH = 9i 

608Y_AXIS_LENGTH = 6i 

609X_ORIGIN = 1i 

610Y_ORIGIN = 1i 

611UNIX_TIME = FALSE 

612UNIX_TIME_POS = BL/-0.75i/-0.75i 

613UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S 

614#-------- Color System Parameters ----------- 

615COLOR_BACKGROUND = 0/0/0 

616COLOR_FOREGROUND = 255/255/255 

617COLOR_NAN = 128/128/128 

618COLOR_IMAGE = adobe 

619COLOR_MODEL = rgb 

620HSV_MIN_SATURATION = 1 

621HSV_MAX_SATURATION = 0.1 

622HSV_MIN_VALUE = 0.3 

623HSV_MAX_VALUE = 1 

624#-------- PostScript Parameters ------------- 

625CHAR_ENCODING = ISOLatin1+ 

626DOTS_PR_INCH = 300 

627N_COPIES = 1 

628PS_COLOR = rgb 

629PS_IMAGE_COMPRESS = none 

630PS_IMAGE_FORMAT = ascii 

631PS_LINE_CAP = round 

632PS_LINE_JOIN = miter 

633PS_MITER_LIMIT = 35 

634PS_VERBOSE = FALSE 

635GLOBAL_X_SCALE = 1 

636GLOBAL_Y_SCALE = 1 

637#-------- I/O Format Parameters ------------- 

638D_FORMAT = %lg 

639FIELD_DELIMITER = tab 

640GRIDFILE_SHORTHAND = FALSE 

641GRID_FORMAT = nf 

642INPUT_CLOCK_FORMAT = hh:mm:ss 

643INPUT_DATE_FORMAT = yyyy-mm-dd 

644IO_HEADER = FALSE 

645N_HEADER_RECS = 1 

646OUTPUT_CLOCK_FORMAT = hh:mm:ss 

647OUTPUT_DATE_FORMAT = yyyy-mm-dd 

648OUTPUT_DEGREE_FORMAT = +D 

649XY_TOGGLE = FALSE 

650#-------- Projection Parameters ------------- 

651ELLIPSOID = WGS-84 

652MAP_SCALE_FACTOR = default 

653MEASURE_UNIT = inch 

654#-------- Calendar/Time Parameters ---------- 

655TIME_FORMAT_PRIMARY = full 

656TIME_FORMAT_SECONDARY = full 

657TIME_EPOCH = 2000-01-01T00:00:00 

658TIME_IS_INTERVAL = OFF 

659TIME_INTERVAL_FRACTION = 0.5 

660TIME_LANGUAGE = us 

661TIME_UNIT = d 

662TIME_WEEK_START = Sunday 

663Y2K_OFFSET_YEAR = 1950 

664#-------- Miscellaneous Parameters ---------- 

665HISTORY = TRUE 

666INTERPOLANT = akima 

667LINE_STEP = 0.01i 

668VECTOR_SHAPE = 0 

669VERBOSE = FALSE''' 

670 

671 

672_gmt_defaults_by_version['4.4.0'] = r''' 

673# 

674# GMT-SYSTEM 4.4.0 [64-bit] Defaults file 

675# 

676#-------- Plot Media Parameters ------------- 

677PAGE_COLOR = 255/255/255 

678PAGE_ORIENTATION = portrait 

679PAPER_MEDIA = a4+ 

680#-------- Basemap Annotation Parameters ------ 

681ANNOT_MIN_ANGLE = 20 

682ANNOT_MIN_SPACING = 0 

683ANNOT_FONT_PRIMARY = Helvetica 

684ANNOT_FONT_SIZE_PRIMARY = 14p 

685ANNOT_OFFSET_PRIMARY = 0.075i 

686ANNOT_FONT_SECONDARY = Helvetica 

687ANNOT_FONT_SIZE_SECONDARY = 16p 

688ANNOT_OFFSET_SECONDARY = 0.075i 

689DEGREE_SYMBOL = ring 

690HEADER_FONT = Helvetica 

691HEADER_FONT_SIZE = 36p 

692HEADER_OFFSET = 0.1875i 

693LABEL_FONT = Helvetica 

694LABEL_FONT_SIZE = 14p 

695LABEL_OFFSET = 0.1125i 

696OBLIQUE_ANNOTATION = 1 

697PLOT_CLOCK_FORMAT = hh:mm:ss 

698PLOT_DATE_FORMAT = yyyy-mm-dd 

699PLOT_DEGREE_FORMAT = +ddd:mm:ss 

700Y_AXIS_TYPE = hor_text 

701#-------- Basemap Layout Parameters --------- 

702BASEMAP_AXES = WESN 

703BASEMAP_FRAME_RGB = 0/0/0 

704BASEMAP_TYPE = plain 

705FRAME_PEN = 1.25p 

706FRAME_WIDTH = 0.075i 

707GRID_CROSS_SIZE_PRIMARY = 0i 

708GRID_PEN_PRIMARY = 0.25p 

709GRID_CROSS_SIZE_SECONDARY = 0i 

710GRID_PEN_SECONDARY = 0.5p 

711MAP_SCALE_HEIGHT = 0.075i 

712POLAR_CAP = 85/90 

713TICK_LENGTH = 0.075i 

714TICK_PEN = 0.5p 

715X_AXIS_LENGTH = 9i 

716Y_AXIS_LENGTH = 6i 

717X_ORIGIN = 1i 

718Y_ORIGIN = 1i 

719UNIX_TIME = FALSE 

720UNIX_TIME_POS = BL/-0.75i/-0.75i 

721UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S 

722#-------- Color System Parameters ----------- 

723COLOR_BACKGROUND = 0/0/0 

724COLOR_FOREGROUND = 255/255/255 

725COLOR_NAN = 128/128/128 

726COLOR_IMAGE = adobe 

727COLOR_MODEL = rgb 

728HSV_MIN_SATURATION = 1 

729HSV_MAX_SATURATION = 0.1 

730HSV_MIN_VALUE = 0.3 

731HSV_MAX_VALUE = 1 

732#-------- PostScript Parameters ------------- 

733CHAR_ENCODING = ISOLatin1+ 

734DOTS_PR_INCH = 300 

735N_COPIES = 1 

736PS_COLOR = rgb 

737PS_IMAGE_COMPRESS = lzw 

738PS_IMAGE_FORMAT = ascii 

739PS_LINE_CAP = round 

740PS_LINE_JOIN = miter 

741PS_MITER_LIMIT = 35 

742PS_VERBOSE = FALSE 

743GLOBAL_X_SCALE = 1 

744GLOBAL_Y_SCALE = 1 

745#-------- I/O Format Parameters ------------- 

746D_FORMAT = %lg 

747FIELD_DELIMITER = tab 

748GRIDFILE_SHORTHAND = FALSE 

749GRID_FORMAT = nf 

750INPUT_CLOCK_FORMAT = hh:mm:ss 

751INPUT_DATE_FORMAT = yyyy-mm-dd 

752IO_HEADER = FALSE 

753N_HEADER_RECS = 1 

754OUTPUT_CLOCK_FORMAT = hh:mm:ss 

755OUTPUT_DATE_FORMAT = yyyy-mm-dd 

756OUTPUT_DEGREE_FORMAT = +D 

757XY_TOGGLE = FALSE 

758#-------- Projection Parameters ------------- 

759ELLIPSOID = WGS-84 

760MAP_SCALE_FACTOR = default 

761MEASURE_UNIT = inch 

762#-------- Calendar/Time Parameters ---------- 

763TIME_FORMAT_PRIMARY = full 

764TIME_FORMAT_SECONDARY = full 

765TIME_EPOCH = 2000-01-01T00:00:00 

766TIME_IS_INTERVAL = OFF 

767TIME_INTERVAL_FRACTION = 0.5 

768TIME_LANGUAGE = us 

769TIME_UNIT = d 

770TIME_WEEK_START = Sunday 

771Y2K_OFFSET_YEAR = 1950 

772#-------- Miscellaneous Parameters ---------- 

773HISTORY = TRUE 

774INTERPOLANT = akima 

775LINE_STEP = 0.01i 

776VECTOR_SHAPE = 0 

777VERBOSE = FALSE 

778''' 

779 

780_gmt_defaults_by_version['4.5.2'] = r''' 

781# 

782# GMT-SYSTEM 4.5.2 [64-bit] Defaults file 

783# 

784#-------- Plot Media Parameters ------------- 

785PAGE_COLOR = white 

786PAGE_ORIENTATION = portrait 

787PAPER_MEDIA = a4+ 

788#-------- Basemap Annotation Parameters ------ 

789ANNOT_MIN_ANGLE = 20 

790ANNOT_MIN_SPACING = 0 

791ANNOT_FONT_PRIMARY = Helvetica 

792ANNOT_FONT_SIZE_PRIMARY = 14p 

793ANNOT_OFFSET_PRIMARY = 0.075i 

794ANNOT_FONT_SECONDARY = Helvetica 

795ANNOT_FONT_SIZE_SECONDARY = 16p 

796ANNOT_OFFSET_SECONDARY = 0.075i 

797DEGREE_SYMBOL = ring 

798HEADER_FONT = Helvetica 

799HEADER_FONT_SIZE = 36p 

800HEADER_OFFSET = 0.1875i 

801LABEL_FONT = Helvetica 

802LABEL_FONT_SIZE = 14p 

803LABEL_OFFSET = 0.1125i 

804OBLIQUE_ANNOTATION = 1 

805PLOT_CLOCK_FORMAT = hh:mm:ss 

806PLOT_DATE_FORMAT = yyyy-mm-dd 

807PLOT_DEGREE_FORMAT = +ddd:mm:ss 

808Y_AXIS_TYPE = hor_text 

809#-------- Basemap Layout Parameters --------- 

810BASEMAP_AXES = WESN 

811BASEMAP_FRAME_RGB = black 

812BASEMAP_TYPE = plain 

813FRAME_PEN = 1.25p 

814FRAME_WIDTH = 0.075i 

815GRID_CROSS_SIZE_PRIMARY = 0i 

816GRID_PEN_PRIMARY = 0.25p 

817GRID_CROSS_SIZE_SECONDARY = 0i 

818GRID_PEN_SECONDARY = 0.5p 

819MAP_SCALE_HEIGHT = 0.075i 

820POLAR_CAP = 85/90 

821TICK_LENGTH = 0.075i 

822TICK_PEN = 0.5p 

823X_AXIS_LENGTH = 9i 

824Y_AXIS_LENGTH = 6i 

825X_ORIGIN = 1i 

826Y_ORIGIN = 1i 

827UNIX_TIME = FALSE 

828UNIX_TIME_POS = BL/-0.75i/-0.75i 

829UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S 

830#-------- Color System Parameters ----------- 

831COLOR_BACKGROUND = black 

832COLOR_FOREGROUND = white 

833COLOR_NAN = 128 

834COLOR_IMAGE = adobe 

835COLOR_MODEL = rgb 

836HSV_MIN_SATURATION = 1 

837HSV_MAX_SATURATION = 0.1 

838HSV_MIN_VALUE = 0.3 

839HSV_MAX_VALUE = 1 

840#-------- PostScript Parameters ------------- 

841CHAR_ENCODING = ISOLatin1+ 

842DOTS_PR_INCH = 300 

843GLOBAL_X_SCALE = 1 

844GLOBAL_Y_SCALE = 1 

845N_COPIES = 1 

846PS_COLOR = rgb 

847PS_IMAGE_COMPRESS = lzw 

848PS_IMAGE_FORMAT = ascii 

849PS_LINE_CAP = round 

850PS_LINE_JOIN = miter 

851PS_MITER_LIMIT = 35 

852PS_VERBOSE = FALSE 

853TRANSPARENCY = 0 

854#-------- I/O Format Parameters ------------- 

855D_FORMAT = %.12lg 

856FIELD_DELIMITER = tab 

857GRIDFILE_FORMAT = nf 

858GRIDFILE_SHORTHAND = FALSE 

859INPUT_CLOCK_FORMAT = hh:mm:ss 

860INPUT_DATE_FORMAT = yyyy-mm-dd 

861IO_HEADER = FALSE 

862N_HEADER_RECS = 1 

863NAN_RECORDS = pass 

864OUTPUT_CLOCK_FORMAT = hh:mm:ss 

865OUTPUT_DATE_FORMAT = yyyy-mm-dd 

866OUTPUT_DEGREE_FORMAT = D 

867XY_TOGGLE = FALSE 

868#-------- Projection Parameters ------------- 

869ELLIPSOID = WGS-84 

870MAP_SCALE_FACTOR = default 

871MEASURE_UNIT = inch 

872#-------- Calendar/Time Parameters ---------- 

873TIME_FORMAT_PRIMARY = full 

874TIME_FORMAT_SECONDARY = full 

875TIME_EPOCH = 2000-01-01T00:00:00 

876TIME_IS_INTERVAL = OFF 

877TIME_INTERVAL_FRACTION = 0.5 

878TIME_LANGUAGE = us 

879TIME_UNIT = d 

880TIME_WEEK_START = Sunday 

881Y2K_OFFSET_YEAR = 1950 

882#-------- Miscellaneous Parameters ---------- 

883HISTORY = TRUE 

884INTERPOLANT = akima 

885LINE_STEP = 0.01i 

886VECTOR_SHAPE = 0 

887VERBOSE = FALSE 

888''' 

889 

890_gmt_defaults_by_version['4.5.3'] = r''' 

891# 

892# GMT-SYSTEM 4.5.3 (CVS Jun 18 2010 10:56:07) [64-bit] Defaults file 

893# 

894#-------- Plot Media Parameters ------------- 

895PAGE_COLOR = white 

896PAGE_ORIENTATION = portrait 

897PAPER_MEDIA = a4+ 

898#-------- Basemap Annotation Parameters ------ 

899ANNOT_MIN_ANGLE = 20 

900ANNOT_MIN_SPACING = 0 

901ANNOT_FONT_PRIMARY = Helvetica 

902ANNOT_FONT_SIZE_PRIMARY = 14p 

903ANNOT_OFFSET_PRIMARY = 0.075i 

904ANNOT_FONT_SECONDARY = Helvetica 

905ANNOT_FONT_SIZE_SECONDARY = 16p 

906ANNOT_OFFSET_SECONDARY = 0.075i 

907DEGREE_SYMBOL = ring 

908HEADER_FONT = Helvetica 

909HEADER_FONT_SIZE = 36p 

910HEADER_OFFSET = 0.1875i 

911LABEL_FONT = Helvetica 

912LABEL_FONT_SIZE = 14p 

913LABEL_OFFSET = 0.1125i 

914OBLIQUE_ANNOTATION = 1 

915PLOT_CLOCK_FORMAT = hh:mm:ss 

916PLOT_DATE_FORMAT = yyyy-mm-dd 

917PLOT_DEGREE_FORMAT = +ddd:mm:ss 

918Y_AXIS_TYPE = hor_text 

919#-------- Basemap Layout Parameters --------- 

920BASEMAP_AXES = WESN 

921BASEMAP_FRAME_RGB = black 

922BASEMAP_TYPE = plain 

923FRAME_PEN = 1.25p 

924FRAME_WIDTH = 0.075i 

925GRID_CROSS_SIZE_PRIMARY = 0i 

926GRID_PEN_PRIMARY = 0.25p 

927GRID_CROSS_SIZE_SECONDARY = 0i 

928GRID_PEN_SECONDARY = 0.5p 

929MAP_SCALE_HEIGHT = 0.075i 

930POLAR_CAP = 85/90 

931TICK_LENGTH = 0.075i 

932TICK_PEN = 0.5p 

933X_AXIS_LENGTH = 9i 

934Y_AXIS_LENGTH = 6i 

935X_ORIGIN = 1i 

936Y_ORIGIN = 1i 

937UNIX_TIME = FALSE 

938UNIX_TIME_POS = BL/-0.75i/-0.75i 

939UNIX_TIME_FORMAT = %Y %b %d %H:%M:%S 

940#-------- Color System Parameters ----------- 

941COLOR_BACKGROUND = black 

942COLOR_FOREGROUND = white 

943COLOR_NAN = 128 

944COLOR_IMAGE = adobe 

945COLOR_MODEL = rgb 

946HSV_MIN_SATURATION = 1 

947HSV_MAX_SATURATION = 0.1 

948HSV_MIN_VALUE = 0.3 

949HSV_MAX_VALUE = 1 

950#-------- PostScript Parameters ------------- 

951CHAR_ENCODING = ISOLatin1+ 

952DOTS_PR_INCH = 300 

953GLOBAL_X_SCALE = 1 

954GLOBAL_Y_SCALE = 1 

955N_COPIES = 1 

956PS_COLOR = rgb 

957PS_IMAGE_COMPRESS = lzw 

958PS_IMAGE_FORMAT = ascii 

959PS_LINE_CAP = round 

960PS_LINE_JOIN = miter 

961PS_MITER_LIMIT = 35 

962PS_VERBOSE = FALSE 

963TRANSPARENCY = 0 

964#-------- I/O Format Parameters ------------- 

965D_FORMAT = %.12lg 

966FIELD_DELIMITER = tab 

967GRIDFILE_FORMAT = nf 

968GRIDFILE_SHORTHAND = FALSE 

969INPUT_CLOCK_FORMAT = hh:mm:ss 

970INPUT_DATE_FORMAT = yyyy-mm-dd 

971IO_HEADER = FALSE 

972N_HEADER_RECS = 1 

973NAN_RECORDS = pass 

974OUTPUT_CLOCK_FORMAT = hh:mm:ss 

975OUTPUT_DATE_FORMAT = yyyy-mm-dd 

976OUTPUT_DEGREE_FORMAT = D 

977XY_TOGGLE = FALSE 

978#-------- Projection Parameters ------------- 

979ELLIPSOID = WGS-84 

980MAP_SCALE_FACTOR = default 

981MEASURE_UNIT = inch 

982#-------- Calendar/Time Parameters ---------- 

983TIME_FORMAT_PRIMARY = full 

984TIME_FORMAT_SECONDARY = full 

985TIME_EPOCH = 2000-01-01T00:00:00 

986TIME_IS_INTERVAL = OFF 

987TIME_INTERVAL_FRACTION = 0.5 

988TIME_LANGUAGE = us 

989TIME_UNIT = d 

990TIME_WEEK_START = Sunday 

991Y2K_OFFSET_YEAR = 1950 

992#-------- Miscellaneous Parameters ---------- 

993HISTORY = TRUE 

994INTERPOLANT = akima 

995LINE_STEP = 0.01i 

996VECTOR_SHAPE = 0 

997VERBOSE = FALSE 

998''' 

999 

1000_gmt_defaults_by_version['5.1.2'] = r''' 

1001# 

1002# GMT 5.1.2 Defaults file 

1003# vim:sw=8:ts=8:sts=8 

1004# $Revision: 13836 $ 

1005# $LastChangedDate: 2014-12-20 03:45:42 -1000 (Sat, 20 Dec 2014) $ 

1006# 

1007# COLOR Parameters 

1008# 

1009COLOR_BACKGROUND = black 

1010COLOR_FOREGROUND = white 

1011COLOR_NAN = 127.5 

1012COLOR_MODEL = none 

1013COLOR_HSV_MIN_S = 1 

1014COLOR_HSV_MAX_S = 0.1 

1015COLOR_HSV_MIN_V = 0.3 

1016COLOR_HSV_MAX_V = 1 

1017# 

1018# DIR Parameters 

1019# 

1020DIR_DATA = 

1021DIR_DCW = 

1022DIR_GSHHG = 

1023# 

1024# FONT Parameters 

1025# 

1026FONT_ANNOT_PRIMARY = 14p,Helvetica,black 

1027FONT_ANNOT_SECONDARY = 16p,Helvetica,black 

1028FONT_LABEL = 14p,Helvetica,black 

1029FONT_LOGO = 8p,Helvetica,black 

1030FONT_TITLE = 24p,Helvetica,black 

1031# 

1032# FORMAT Parameters 

1033# 

1034FORMAT_CLOCK_IN = hh:mm:ss 

1035FORMAT_CLOCK_OUT = hh:mm:ss 

1036FORMAT_CLOCK_MAP = hh:mm:ss 

1037FORMAT_DATE_IN = yyyy-mm-dd 

1038FORMAT_DATE_OUT = yyyy-mm-dd 

1039FORMAT_DATE_MAP = yyyy-mm-dd 

1040FORMAT_GEO_OUT = D 

1041FORMAT_GEO_MAP = ddd:mm:ss 

1042FORMAT_FLOAT_OUT = %.12g 

1043FORMAT_FLOAT_MAP = %.12g 

1044FORMAT_TIME_PRIMARY_MAP = full 

1045FORMAT_TIME_SECONDARY_MAP = full 

1046FORMAT_TIME_STAMP = %Y %b %d %H:%M:%S 

1047# 

1048# GMT Miscellaneous Parameters 

1049# 

1050GMT_COMPATIBILITY = 4 

1051GMT_CUSTOM_LIBS = 

1052GMT_EXTRAPOLATE_VAL = NaN 

1053GMT_FFT = auto 

1054GMT_HISTORY = true 

1055GMT_INTERPOLANT = akima 

1056GMT_TRIANGULATE = Shewchuk 

1057GMT_VERBOSE = compat 

1058GMT_LANGUAGE = us 

1059# 

1060# I/O Parameters 

1061# 

1062IO_COL_SEPARATOR = tab 

1063IO_GRIDFILE_FORMAT = nf 

1064IO_GRIDFILE_SHORTHAND = false 

1065IO_HEADER = false 

1066IO_N_HEADER_RECS = 0 

1067IO_NAN_RECORDS = pass 

1068IO_NC4_CHUNK_SIZE = auto 

1069IO_NC4_DEFLATION_LEVEL = 3 

1070IO_LONLAT_TOGGLE = false 

1071IO_SEGMENT_MARKER = > 

1072# 

1073# MAP Parameters 

1074# 

1075MAP_ANNOT_MIN_ANGLE = 20 

1076MAP_ANNOT_MIN_SPACING = 0p 

1077MAP_ANNOT_OBLIQUE = 1 

1078MAP_ANNOT_OFFSET_PRIMARY = 0.075i 

1079MAP_ANNOT_OFFSET_SECONDARY = 0.075i 

1080MAP_ANNOT_ORTHO = we 

1081MAP_DEFAULT_PEN = default,black 

1082MAP_DEGREE_SYMBOL = ring 

1083MAP_FRAME_AXES = WESNZ 

1084MAP_FRAME_PEN = thicker,black 

1085MAP_FRAME_TYPE = fancy 

1086MAP_FRAME_WIDTH = 5p 

1087MAP_GRID_CROSS_SIZE_PRIMARY = 0p 

1088MAP_GRID_CROSS_SIZE_SECONDARY = 0p 

1089MAP_GRID_PEN_PRIMARY = default,black 

1090MAP_GRID_PEN_SECONDARY = thinner,black 

1091MAP_LABEL_OFFSET = 0.1944i 

1092MAP_LINE_STEP = 0.75p 

1093MAP_LOGO = false 

1094MAP_LOGO_POS = BL/-54p/-54p 

1095MAP_ORIGIN_X = 1i 

1096MAP_ORIGIN_Y = 1i 

1097MAP_POLAR_CAP = 85/90 

1098MAP_SCALE_HEIGHT = 5p 

1099MAP_TICK_LENGTH_PRIMARY = 5p/2.5p 

1100MAP_TICK_LENGTH_SECONDARY = 15p/3.75p 

1101MAP_TICK_PEN_PRIMARY = thinner,black 

1102MAP_TICK_PEN_SECONDARY = thinner,black 

1103MAP_TITLE_OFFSET = 14p 

1104MAP_VECTOR_SHAPE = 0 

1105# 

1106# Projection Parameters 

1107# 

1108PROJ_AUX_LATITUDE = authalic 

1109PROJ_ELLIPSOID = WGS-84 

1110PROJ_LENGTH_UNIT = cm 

1111PROJ_MEAN_RADIUS = authalic 

1112PROJ_SCALE_FACTOR = default 

1113# 

1114# PostScript Parameters 

1115# 

1116PS_CHAR_ENCODING = ISOLatin1+ 

1117PS_COLOR_MODEL = rgb 

1118PS_COMMENTS = false 

1119PS_IMAGE_COMPRESS = deflate,5 

1120PS_LINE_CAP = butt 

1121PS_LINE_JOIN = miter 

1122PS_MITER_LIMIT = 35 

1123PS_MEDIA = a4 

1124PS_PAGE_COLOR = white 

1125PS_PAGE_ORIENTATION = portrait 

1126PS_SCALE_X = 1 

1127PS_SCALE_Y = 1 

1128PS_TRANSPARENCY = Normal 

1129# 

1130# Calendar/Time Parameters 

1131# 

1132TIME_EPOCH = 1970-01-01T00:00:00 

1133TIME_IS_INTERVAL = off 

1134TIME_INTERVAL_FRACTION = 0.5 

1135TIME_UNIT = s 

1136TIME_WEEK_START = Monday 

1137TIME_Y2K_OFFSET_YEAR = 1950 

1138''' 

1139 

1140 

1141def get_gmt_version(gmtdefaultsbinary, gmthomedir=None): 

1142 args = [gmtdefaultsbinary] 

1143 

1144 environ = os.environ.copy() 

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

1146 

1147 p = subprocess.Popen( 

1148 args, 

1149 stdout=subprocess.PIPE, 

1150 stderr=subprocess.PIPE, 

1151 env=environ) 

1152 

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

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

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

1156 

1157 if not m: 

1158 raise GMTInstallationProblem( 

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

1160 % gmtdefaultsbinary) 

1161 

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

1163 

1164 

1165def detect_gmt_installations(): 

1166 

1167 installations = {} 

1168 errmesses = [] 

1169 

1170 # GMT 4.x: 

1171 try: 

1172 p = subprocess.Popen( 

1173 ['GMT'], 

1174 stdout=subprocess.PIPE, 

1175 stderr=subprocess.PIPE) 

1176 

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

1178 

1179 m = re.search(br'Version\s+(\d+(\.\d+)*)', stderr, re.M) 

1180 if not m: 

1181 raise GMTInstallationProblem( 

1182 "Can't get version number from output of GMT.") 

1183 

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

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

1186 

1187 m = re.search(br'^\s+executables\s+(.+)$', stderr, re.M) 

1188 if not m: 

1189 raise GMTInstallationProblem( 

1190 "Can't extract executables dir from output of GMT.") 

1191 

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

1193 

1194 m = re.search(br'^\s+shared data\s+(.+)$', stderr, re.M) 

1195 if not m: 

1196 raise GMTInstallationProblem( 

1197 "Can't extract shared dir from output of GMT.") 

1198 

1199 gmtshare = str(m.group(1).decode('ascii')) 

1200 if not gmtshare.endswith('/share'): 

1201 raise GMTInstallationProblem( 

1202 "Can't determine GMTHOME from output of GMT.") 

1203 

1204 gmthome = gmtshare[:-6] 

1205 

1206 installations[version] = { 

1207 'home': gmthome, 

1208 'bin': gmtbin} 

1209 

1210 except OSError as e: 

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

1212 

1213 try: 

1214 version = str(subprocess.check_output( 

1215 ['gmt', '--version']).strip().decode('ascii')).split('_')[0] 

1216 gmtbin = str(subprocess.check_output( 

1217 ['gmt', '--show-bindir']).strip().decode('ascii')) 

1218 installations[version] = { 

1219 'bin': gmtbin} 

1220 

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

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

1223 

1224 if not installations: 

1225 s = [] 

1226 for (progname, errmess) in errmesses: 

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

1228 

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

1230 

1231 return installations 

1232 

1233 

1234def appropriate_defaults_version(version): 

1235 avails = sorted(_gmt_defaults_by_version.keys(), key=key_version) 

1236 for iavail, avail in enumerate(avails): 

1237 if key_version(version) == key_version(avail): 

1238 return version 

1239 

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

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

1242 

1243 return avails[-1] 

1244 

1245 

1246def gmt_default_config(version): 

1247 ''' 

1248 Get default GMT configuration dict for given version. 

1249 ''' 

1250 

1251 xversion = appropriate_defaults_version(version) 

1252 

1253 # if not version in _gmt_defaults_by_version: 

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

1255 

1256 gmt_defaults = _gmt_defaults_by_version[xversion] 

1257 

1258 d = {} 

1259 for line in gmt_defaults.splitlines(): 

1260 sline = line.strip() 

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

1262 continue 

1263 

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

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

1266 

1267 return d 

1268 

1269 

1270def diff_defaults(v1, v2): 

1271 d1 = gmt_default_config(v1) 

1272 d2 = gmt_default_config(v2) 

1273 for k in d1: 

1274 if k not in d2: 

1275 print('%s not in %s' % (k, v2)) 

1276 else: 

1277 if d1[k] != d2[k]: 

1278 print('%s %s = %s' % (v1, k, d1[k])) 

1279 print('%s %s = %s' % (v2, k, d2[k])) 

1280 

1281 for k in d2: 

1282 if k not in d1: 

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

1284 

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

1286 

1287 

1288def check_gmt_installation(installation): 

1289 

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

1291 bin_dir = installation['bin'] 

1292 version = installation['version'] 

1293 

1294 for d in home_dir, bin_dir: 

1295 if d is not None: 

1296 if not os.path.exists(d): 

1297 logging.error(('Directory does not exist: %s\n' 

1298 'Check your GMT installation.') % d) 

1299 

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

1301 

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

1303 gmtdefaults = pjoin(bin_dir, 'gmtdefaults') 

1304 

1305 versionfound = get_gmt_version(gmtdefaults, home_dir) 

1306 

1307 if versionfound != version: 

1308 raise GMTInstallationProblem(( 

1309 'Expected GMT version %s but found version %s.\n' 

1310 '(Looking at output of %s)') % ( 

1311 version, versionfound, gmtdefaults)) 

1312 

1313 

1314def get_gmt_installation(version): 

1315 setup_gmt_installations() 

1316 if version != 'newest' and version not in _gmt_installations: 

1317 logging.warn('GMT version %s not installed, taking version %s instead' 

1318 % (version, newest_installed_gmt_version())) 

1319 

1320 version = 'newest' 

1321 

1322 if version == 'newest': 

1323 version = newest_installed_gmt_version() 

1324 

1325 installation = dict(_gmt_installations[version]) 

1326 

1327 return installation 

1328 

1329 

1330def setup_gmt_installations(): 

1331 if not setup_gmt_installations.have_done: 

1332 if not _gmt_installations: 

1333 

1334 _gmt_installations.update(detect_gmt_installations()) 

1335 

1336 # store defaults as dicts into the gmt installations dicts 

1337 for version, installation in _gmt_installations.items(): 

1338 installation['defaults'] = gmt_default_config(version) 

1339 installation['version'] = version 

1340 

1341 for installation in _gmt_installations.values(): 

1342 check_gmt_installation(installation) 

1343 

1344 setup_gmt_installations.have_done = True 

1345 

1346 

1347setup_gmt_installations.have_done = False 

1348 

1349_paper_sizes_a = '''A0 2380 3368 

1350 A1 1684 2380 

1351 A2 1190 1684 

1352 A3 842 1190 

1353 A4 595 842 

1354 A5 421 595 

1355 A6 297 421 

1356 A7 210 297 

1357 A8 148 210 

1358 A9 105 148 

1359 A10 74 105 

1360 B0 2836 4008 

1361 B1 2004 2836 

1362 B2 1418 2004 

1363 B3 1002 1418 

1364 B4 709 1002 

1365 B5 501 709 

1366 archA 648 864 

1367 archB 864 1296 

1368 archC 1296 1728 

1369 archD 1728 2592 

1370 archE 2592 3456 

1371 flsa 612 936 

1372 halfletter 396 612 

1373 note 540 720 

1374 letter 612 792 

1375 legal 612 1008 

1376 11x17 792 1224 

1377 ledger 1224 792''' 

1378 

1379 

1380_paper_sizes = {} 

1381 

1382 

1383def setup_paper_sizes(): 

1384 if not _paper_sizes: 

1385 for line in _paper_sizes_a.splitlines(): 

1386 k, w, h = line.split() 

1387 _paper_sizes[k.lower()] = float(w), float(h) 

1388 

1389 

1390def get_paper_size(k): 

1391 setup_paper_sizes() 

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

1393 

1394 

1395def all_paper_sizes(): 

1396 setup_paper_sizes() 

1397 return _paper_sizes 

1398 

1399 

1400def measure_unit(gmt_config): 

1401 for k in ['MEASURE_UNIT', 'PROJ_LENGTH_UNIT']: 

1402 if k in gmt_config: 

1403 return gmt_config[k] 

1404 

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

1406 

1407 

1408def paper_media(gmt_config): 

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

1410 if k in gmt_config: 

1411 return gmt_config[k] 

1412 

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

1414 

1415 

1416def page_orientation(gmt_config): 

1417 for k in ['PAGE_ORIENTATION', 'PS_PAGE_ORIENTATION']: 

1418 if k in gmt_config: 

1419 return gmt_config[k] 

1420 

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

1422 

1423 

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

1425 

1426 leftmargin, topmargin, rightmargin, bottommargin = margins 

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

1428 

1429 paper_size = get_paper_size(paper_media(gmt_config)) 

1430 if not portrait: 

1431 paper_size = paper_size[1], paper_size[0] 

1432 

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

1434 2.0 + leftmargin 

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

1436 2.0 + bottommargin 

1437 

1438 if portrait: 

1439 bb1 = int((xoffset - leftmargin)) 

1440 bb2 = int((yoffset - bottommargin)) 

1441 bb3 = bb1 + int((width+leftmargin+rightmargin)) 

1442 bb4 = bb2 + int((height+topmargin+bottommargin)) 

1443 else: 

1444 bb1 = int((yoffset - topmargin)) 

1445 bb2 = int((xoffset - leftmargin)) 

1446 bb3 = bb1 + int((height+topmargin+bottommargin)) 

1447 bb4 = bb2 + int((width+leftmargin+rightmargin)) 

1448 

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

1450 

1451 

1452def gmtdefaults_as_text(version='newest'): 

1453 

1454 ''' 

1455 Get the built-in gmtdefaults. 

1456 ''' 

1457 

1458 if version not in _gmt_installations: 

1459 logging.warn('GMT version %s not installed, taking version %s instead' 

1460 % (version, newest_installed_gmt_version())) 

1461 version = 'newest' 

1462 

1463 if version == 'newest': 

1464 version = newest_installed_gmt_version() 

1465 

1466 return _gmt_defaults_by_version[version] 

1467 

1468 

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

1470 ''' 

1471 Write COARDS compliant netcdf (grd) file. 

1472 ''' 

1473 

1474 assert y.size, x.size == z.shape 

1475 ny, nx = z.shape 

1476 nc = netcdf.netcdf_file(filename, 'w') 

1477 assert naming in ('xy', 'lonlat') 

1478 

1479 if naming == 'xy': 

1480 kx, ky = 'x', 'y' 

1481 else: 

1482 kx, ky = 'lon', 'lat' 

1483 

1484 nc.node_offset = 0 

1485 if title is not None: 

1486 nc.title = title 

1487 

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

1489 nc.createDimension(kx, nx) 

1490 nc.createDimension(ky, ny) 

1491 

1492 xvar = nc.createVariable(kx, 'd', (kx,)) 

1493 yvar = nc.createVariable(ky, 'd', (ky,)) 

1494 if naming == 'xy': 

1495 xvar.long_name = kx 

1496 yvar.long_name = ky 

1497 else: 

1498 xvar.long_name = 'longitude' 

1499 xvar.units = 'degrees_east' 

1500 yvar.long_name = 'latitude' 

1501 yvar.units = 'degrees_north' 

1502 

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

1504 

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

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

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

1508 

1509 nc.close() 

1510 

1511 

1512def to_array(var): 

1513 arr = var[:].copy() 

1514 if hasattr(var, 'scale_factor'): 

1515 arr *= var.scale_factor 

1516 

1517 if hasattr(var, 'add_offset'): 

1518 arr += var.add_offset 

1519 

1520 return arr 

1521 

1522 

1523def loadgrd(filename): 

1524 ''' 

1525 Read COARDS compliant netcdf (grd) file. 

1526 ''' 

1527 

1528 nc = netcdf.netcdf_file(filename, 'r') 

1529 vkeys = list(nc.variables.keys()) 

1530 kx = 'x' 

1531 ky = 'y' 

1532 if 'lon' in vkeys: 

1533 kx = 'lon' 

1534 if 'lat' in vkeys: 

1535 ky = 'lat' 

1536 

1537 kz = 'z' 

1538 if 'altitude' in vkeys: 

1539 kz = 'altitude' 

1540 

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

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

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

1544 

1545 nc.close() 

1546 return x, y, z 

1547 

1548 

1549def centers_to_edges(asorted): 

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

1551 

1552 

1553def nvals(asorted): 

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

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

1556 

1557 

1558def guess_vals(asorted): 

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

1560 indis = num.nonzero(asorted[1:] - asorted[:-1] >= eps)[0] 

1561 indis = num.concatenate((num.array([0]), indis+1, 

1562 num.array([asorted.size]))) 

1563 asum = num.zeros(asorted.size+1) 

1564 asum[1:] = num.cumsum(asorted) 

1565 return (asum[indis[1:]] - asum[indis[:-1]]) / (indis[1:]-indis[:-1]) 

1566 

1567 

1568def blockmean(asorted, b): 

1569 indis = num.nonzero(asorted[1:] - asorted[:-1])[0] 

1570 indis = num.concatenate((num.array([0]), indis+1, 

1571 num.array([asorted.size]))) 

1572 bsum = num.zeros(b.size+1) 

1573 bsum[1:] = num.cumsum(b) 

1574 return ( 

1575 asorted[indis[:-1]], 

1576 (bsum[indis[1:]] - bsum[indis[:-1]]) / (indis[1:]-indis[:-1])) 

1577 

1578 

1579def griddata_regular(x, y, z, xvals, yvals): 

1580 nx, ny = xvals.size, yvals.size 

1581 xindi = num.digitize(x, centers_to_edges(xvals)) 

1582 yindi = num.digitize(y, centers_to_edges(yvals)) 

1583 

1584 zindi = yindi*nx+xindi 

1585 order = num.argsort(zindi) 

1586 z = z[order] 

1587 zindi = zindi[order] 

1588 

1589 zindi, z = blockmean(zindi, z) 

1590 znew = num.empty(nx*ny, dtype=float) 

1591 znew[:] = num.nan 

1592 znew[zindi] = z 

1593 return znew.reshape(ny, nx) 

1594 

1595 

1596def guess_field_size(x_sorted, y_sorted, z=None, mode=None): 

1597 critical_fraction = 1./num.e - 0.014*3 

1598 xs = x_sorted 

1599 ys = y_sorted 

1600 nxs, nys = nvals(xs), nvals(ys) 

1601 if mode == 'nonrandom': 

1602 return nxs, nys, 0 

1603 elif xs.size == nxs*nys: 

1604 # exact match 

1605 return nxs, nys, 0 

1606 elif nxs >= xs.size*critical_fraction and nys >= xs.size*critical_fraction: 

1607 # possibly randomly sampled 

1608 nxs = int(math.sqrt(xs.size)) 

1609 nys = nxs 

1610 return nxs, nys, 2 

1611 else: 

1612 return nxs, nys, 1 

1613 

1614 

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

1616 ''' 

1617 Grid tabular XYZ data by binning. 

1618 

1619 This function does some extra work to guess the size of the grid. This 

1620 should work fine if the input values are already defined on an rectilinear 

1621 grid, even if data points are missing or duplicated. This routine also 

1622 tries to detect a random distribution of input data and in that case 

1623 creates a grid of size sqrt(N) x sqrt(N). 

1624 

1625 The points do not have to be given in any particular order. Grid nodes 

1626 without data are assigned the NaN value. If multiple data points map to the 

1627 same grid node, their average is assigned to the grid node. 

1628 ''' 

1629 

1630 x, y, z = [num.asarray(X) for X in (x, y, z)] 

1631 assert x.size == y.size == z.size 

1632 xs, ys = num.sort(x), num.sort(y) 

1633 nx, ny, badness = guess_field_size(xs, ys, z, mode=mode) 

1634 if badness <= 1: 

1635 xf = guess_vals(xs) 

1636 yf = guess_vals(ys) 

1637 zf = griddata_regular(x, y, z, xf, yf) 

1638 else: 

1639 xf = num.linspace(xs[0], xs[-1], nx) 

1640 yf = num.linspace(ys[0], ys[-1], ny) 

1641 zf = griddata_regular(x, y, z, xf, yf) 

1642 

1643 return xf, yf, zf 

1644 

1645 

1646def tabledata(xf, yf, zf): 

1647 assert yf.size, xf.size == zf.shape 

1648 x = num.tile(xf, yf.size) 

1649 y = num.repeat(yf, xf.size) 

1650 z = zf.flatten() 

1651 return x, y, z 

1652 

1653 

1654def double1d(a): 

1655 a2 = num.empty(a.size*2-1) 

1656 a2[::2] = a 

1657 a2[1::2] = (a[:-1] + a[1:])/2. 

1658 return a2 

1659 

1660 

1661def double2d(f): 

1662 f2 = num.empty((f.shape[0]*2-1, f.shape[1]*2-1)) 

1663 f2[:, :] = num.nan 

1664 f2[::2, ::2] = f 

1665 f2[1::2, ::2] = (f[:-1, :] + f[1:, :])/2. 

1666 f2[::2, 1::2] = (f[:, :-1] + f[:, 1:])/2. 

1667 f2[1::2, 1::2] = (f[:-1, :-1] + f[1:, :-1] + f[:-1, 1:] + f[1:, 1:])/4. 

1668 diag = f2[1::2, 1::2] 

1669 diagA = (f[:-1, :-1] + f[1:, 1:]) / 2. 

1670 diagB = (f[1:, :-1] + f[:-1, 1:]) / 2. 

1671 f2[1::2, 1::2] = num.where(num.isnan(diag), diagA, diag) 

1672 f2[1::2, 1::2] = num.where(num.isnan(diag), diagB, diag) 

1673 return f2 

1674 

1675 

1676def doublegrid(x, y, z): 

1677 x2 = double1d(x) 

1678 y2 = double1d(y) 

1679 z2 = double2d(z) 

1680 return x2, y2, z2 

1681 

1682 

1683class Guru(object): 

1684 ''' 

1685 Abstract base class providing template interpolation, accessible as 

1686 attributes. 

1687 

1688 Classes deriving from this one, have to implement a :py:meth:`get_params` 

1689 method, which is called to get a dict to do ordinary 

1690 ``"%(key)x"``-substitutions. The deriving class must also provide a dict 

1691 with the templates. 

1692 ''' 

1693 

1694 def __init__(self): 

1695 self.templates = {} 

1696 

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

1698 params = self.get_params(**kwargs) 

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

1700 return strings 

1701 

1702 # hand through templates dict 

1703 def __getitem__(self, template_name): 

1704 return self.templates[template_name] 

1705 

1706 def __setitem__(self, template_name, template): 

1707 self.templates[template_name] = template 

1708 

1709 def __contains__(self, template_name): 

1710 return template_name in self.templates 

1711 

1712 def __iter__(self): 

1713 return iter(self.templates) 

1714 

1715 def __len__(self): 

1716 return len(self.templates) 

1717 

1718 def __delitem__(self, template_name): 

1719 del(self.templates[template_name]) 

1720 

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

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

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

1724 

1725 def __getattr__(self, template_names): 

1726 if [n for n in template_names if n not in self.templates]: 

1727 raise AttributeError(template_names) 

1728 

1729 def f(**kwargs): 

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

1731 

1732 return f 

1733 

1734 

1735def nice_value(x): 

1736 ''' 

1737 Round ``x`` to nice value. 

1738 ''' 

1739 

1740 exp = 1.0 

1741 sign = 1 

1742 if x < 0.0: 

1743 x = -x 

1744 sign = -1 

1745 while x >= 1.0: 

1746 x /= 10.0 

1747 exp *= 10.0 

1748 while x < 0.1: 

1749 x *= 10.0 

1750 exp /= 10.0 

1751 

1752 if x >= 0.75: 

1753 return sign * 1.0 * exp 

1754 if x >= 0.375: 

1755 return sign * 0.5 * exp 

1756 if x >= 0.225: 

1757 return sign * 0.25 * exp 

1758 if x >= 0.15: 

1759 return sign * 0.2 * exp 

1760 

1761 return sign * 0.1 * exp 

1762 

1763 

1764class AutoScaler(object): 

1765 ''' 

1766 Tunable 1D autoscaling based on data range. 

1767 

1768 Instances of this class may be used to determine nice minima, maxima and 

1769 increments for ax annotations, as well as suitable common exponents for 

1770 notation. 

1771 

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

1773 

1774 .. py:attribute:: approx_ticks 

1775 

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

1777 

1778 .. py:attribute:: mode 

1779 

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

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

1782 

1783 ================ ================================================== 

1784 mode description 

1785 ================ ================================================== 

1786 ``'auto'``: Look at data range and choose one of the choices 

1787 below. 

1788 ``'min-max'``: Output range is selected to include data range. 

1789 ``'0-max'``: Output range shall start at zero and end at data 

1790 max. 

1791 ``'min-0'``: Output range shall start at data min and end at 

1792 zero. 

1793 ``'symmetric'``: Output range shall by symmetric by zero. 

1794 ``'off'``: Similar to ``'min-max'``, but snap and space are 

1795 disabled, such that the output range always 

1796 exactly matches the data range. 

1797 ================ ================================================== 

1798 

1799 .. py:attribute:: exp 

1800 

1801 If defined, override automatically determined exponent for notation 

1802 by the given value. 

1803 

1804 .. py:attribute:: snap 

1805 

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

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

1808 

1809 .. py:attribute:: inc 

1810 

1811 If defined, override automatically determined tick increment by the 

1812 given value. 

1813 

1814 .. py:attribute:: space 

1815 

1816 Add some padding to the range. The value given, is the fraction by 

1817 which the output range is increased on each side. If mode is 

1818 ``'0-max'`` or ``'min-0'``, the end at zero is kept fixed at zero. 

1819 This parameter has no effect if mode is set to ``'off'``. 

1820 

1821 .. py:attribute:: exp_factor 

1822 

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

1824 

1825 .. py:attribute:: no_exp_interval: 

1826 

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

1828 

1829 ''' 

1830 

1831 def __init__( 

1832 self, 

1833 approx_ticks=7.0, 

1834 mode='auto', 

1835 exp=None, 

1836 snap=False, 

1837 inc=None, 

1838 space=0.0, 

1839 exp_factor=3, 

1840 no_exp_interval=(-3, 5)): 

1841 

1842 ''' 

1843 Create new AutoScaler instance. 

1844 

1845 The parameters are described in the AutoScaler documentation. 

1846 ''' 

1847 

1848 self.approx_ticks = approx_ticks 

1849 self.mode = mode 

1850 self.exp = exp 

1851 self.snap = snap 

1852 self.inc = inc 

1853 self.space = space 

1854 self.exp_factor = exp_factor 

1855 self.no_exp_interval = no_exp_interval 

1856 

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

1858 

1859 ''' 

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

1861 

1862 Returns ``(minimum, maximum, increment)`` or ``(maximum, minimum, 

1863 -increment)``, depending on whether data_range is ``(data_min, 

1864 data_max)`` or ``(data_max, data_min)``. If ``override_mode`` is 

1865 defined, the mode attribute is temporarily overridden by the given 

1866 value. 

1867 ''' 

1868 

1869 data_min = min(data_range) 

1870 data_max = max(data_range) 

1871 

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

1873 

1874 a = self.mode 

1875 if self.mode == 'auto': 

1876 a = self.guess_autoscale_mode(data_min, data_max) 

1877 

1878 if override_mode is not None: 

1879 a = override_mode 

1880 

1881 mi, ma = 0, 0 

1882 if a == 'off': 

1883 mi, ma = data_min, data_max 

1884 elif a == '0-max': 

1885 mi = 0.0 

1886 if data_max > 0.0: 

1887 ma = data_max 

1888 else: 

1889 ma = 1.0 

1890 elif a == 'min-0': 

1891 ma = 0.0 

1892 if data_min < 0.0: 

1893 mi = data_min 

1894 else: 

1895 mi = -1.0 

1896 elif a == 'min-max': 

1897 mi, ma = data_min, data_max 

1898 elif a == 'symmetric': 

1899 m = max(abs(data_min), abs(data_max)) 

1900 mi = -m 

1901 ma = m 

1902 

1903 nmi = mi 

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

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

1906 

1907 nma = ma 

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

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

1910 

1911 mi, ma = nmi, nma 

1912 

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

1914 mi -= 1.0 

1915 ma += 1.0 

1916 

1917 # make nice tick increment 

1918 if self.inc is not None: 

1919 inc = self.inc 

1920 else: 

1921 if self.approx_ticks > 0.: 

1922 inc = nice_value((ma-mi) / self.approx_ticks) 

1923 else: 

1924 inc = nice_value((ma-mi)*10.) 

1925 

1926 if inc == 0.0: 

1927 inc = 1.0 

1928 

1929 # snap min and max to ticks if this is wanted 

1930 if self.snap and a != 'off': 

1931 ma = inc * math.ceil(ma/inc) 

1932 mi = inc * math.floor(mi/inc) 

1933 

1934 if is_reverse: 

1935 return ma, mi, -inc 

1936 else: 

1937 return mi, ma, inc 

1938 

1939 def make_exp(self, x): 

1940 ''' 

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

1942 

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

1944 ''' 

1945 

1946 if self.exp is not None: 

1947 return self.exp 

1948 

1949 x = abs(x) 

1950 if x == 0.0: 

1951 return 0 

1952 

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

1954 return 0 

1955 

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

1957 

1958 def guess_autoscale_mode(self, data_min, data_max): 

1959 ''' 

1960 Guess mode of operation, based on data range. 

1961 

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

1963 or ``'symmetric'``. 

1964 ''' 

1965 

1966 a = 'min-max' 

1967 if data_min >= 0.0: 

1968 if data_min < data_max/2.: 

1969 a = '0-max' 

1970 else: 

1971 a = 'min-max' 

1972 if data_max <= 0.0: 

1973 if data_max > data_min/2.: 

1974 a = 'min-0' 

1975 else: 

1976 a = 'min-max' 

1977 if data_min < 0.0 and data_max > 0.0: 

1978 if abs((abs(data_max)-abs(data_min)) / 

1979 (abs(data_max)+abs(data_min))) < 0.5: 

1980 a = 'symmetric' 

1981 else: 

1982 a = 'min-max' 

1983 return a 

1984 

1985 

1986class Ax(AutoScaler): 

1987 ''' 

1988 Ax description with autoscaling capabilities. 

1989 

1990 The ax is described by the :py:class:`AutoScaler` public attributes, plus 

1991 the following additional attributes (with default values given in 

1992 paranthesis): 

1993 

1994 .. py:attribute:: label 

1995 

1996 Ax label (without unit). 

1997 

1998 .. py:attribute:: unit 

1999 

2000 Physical unit of the data attached to this ax. 

2001 

2002 .. py:attribute:: scaled_unit 

2003 

2004 (see below) 

2005 

2006 .. py:attribute:: scaled_unit_factor 

2007 

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

2009 

2010 unit = scaled_unit_factor x scaled_unit. 

2011 

2012 (E.g. if unit is 'm' and data is in the range of nanometers, you may 

2013 want to set the scaled_unit to 'nm' and the scaled_unit_factor to 

2014 1e9.) 

2015 

2016 .. py:attribute:: limits 

2017 

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

2019 

2020 .. py:attribute:: masking 

2021 

2022 If true and if there is a limit on the ax, while calculating ranges, 

2023 the data points are masked such that data points outside of this axes 

2024 limits are not used to determine the range of another dependant ax. 

2025 

2026 ''' 

2027 

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

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

2030 

2031 AutoScaler.__init__(self, **kwargs) 

2032 self.label = label 

2033 self.unit = unit 

2034 self.scaled_unit_factor = scaled_unit_factor 

2035 self.scaled_unit = scaled_unit 

2036 self.limits = limits 

2037 self.masking = masking 

2038 

2039 def label_str(self, exp, unit): 

2040 ''' 

2041 Get label string including the unit and multiplier. 

2042 ''' 

2043 

2044 slabel, sunit, sexp = '', '', '' 

2045 if self.label: 

2046 slabel = self.label 

2047 

2048 if unit or exp != 0: 

2049 if exp != 0: 

2050 sexp = '\\327 10@+%i@+' % exp 

2051 sunit = '[ %s %s ]' % (sexp, unit) 

2052 else: 

2053 sunit = '[ %s ]' % unit 

2054 

2055 p = [] 

2056 if slabel: 

2057 p.append(slabel) 

2058 

2059 if sunit: 

2060 p.append(sunit) 

2061 

2062 return ' '.join(p) 

2063 

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

2065 override_scaled_unit_factor=None): 

2066 

2067 ''' 

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

2069 

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

2071 multiplier for given data range. 

2072 

2073 If ``ax_projection`` is True, values suitable to be displayed on the ax 

2074 are returned, e.g. min, max and inc are returned in scaled units. 

2075 Otherwise the values are returned in the original units, without any 

2076 scaling applied. 

2077 ''' 

2078 

2079 sf = self.scaled_unit_factor 

2080 

2081 if override_scaled_unit_factor is not None: 

2082 sf = override_scaled_unit_factor 

2083 

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

2085 

2086 mi, ma, inc = self.make_scale(dr_scaled, override_mode=override_mode) 

2087 if self.inc is not None: 

2088 inc = self.inc*sf 

2089 

2090 if ax_projection: 

2091 exp = self.make_exp(inc) 

2092 if sf == 1. and override_scaled_unit_factor is None: 

2093 unit = self.unit 

2094 else: 

2095 unit = self.scaled_unit 

2096 label = self.label_str(exp, unit) 

2097 return mi/10**exp, ma/10**exp, inc/10**exp, label 

2098 else: 

2099 label = self.label_str(0, self.unit) 

2100 return mi/sf, ma/sf, inc/sf, label 

2101 

2102 

2103class ScaleGuru(Guru): 

2104 

2105 ''' 

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

2107 

2108 Instances of this class provide automatic determination of plot ranges, 

2109 tick increments and scaled annotations, as well as label/unit handling. It 

2110 can in particular be used to automatically generate the -R and -B option 

2111 arguments, which are required for most GMT commands. 

2112 

2113 It extends the functionality of the :py:class:`Ax` and 

2114 :py:class:`AutoScaler` classes at the level, where it can not be handled 

2115 anymore by looking at a single dimension of the dataset's data, e.g.: 

2116 

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

2118 

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

2120 limits imposed on other axes. 

2121 

2122 ''' 

2123 

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

2125 percent_interval=None, copy_from=None): 

2126 

2127 Guru.__init__(self) 

2128 

2129 if copy_from: 

2130 self.templates = copy.deepcopy(copy_from.templates) 

2131 self.axes = copy.deepcopy(copy_from.axes) 

2132 self.data_ranges = copy.deepcopy(copy_from.data_ranges) 

2133 self.aspect = copy_from.aspect 

2134 

2135 if percent_interval is not None: 

2136 from scipy.stats import scoreatpercentile as scap 

2137 

2138 self.templates = dict( 

2139 R='-R%(xmin)g/%(xmax)g/%(ymin)g/%(ymax)g', 

2140 B='-B%(xinc)g:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen', 

2141 T='-T%(zmin)g/%(zmax)g/%(zinc)g') 

2142 

2143 maxdim = 2 

2144 if data_tuples: 

2145 maxdim = max(maxdim, max([len(dt) for dt in data_tuples])) 

2146 else: 

2147 if axes: 

2148 maxdim = len(axes) 

2149 data_tuples = [([],) * maxdim] 

2150 if axes is not None: 

2151 self.axes = axes 

2152 else: 

2153 self.axes = [Ax() for i in range(maxdim)] 

2154 

2155 # sophisticated data-range calculation 

2156 data_ranges = [None] * maxdim 

2157 for dt_ in data_tuples: 

2158 dt = num.asarray(dt_) 

2159 in_range = True 

2160 for ax, x in zip(self.axes, dt): 

2161 if ax.limits and ax.masking: 

2162 ax_limits = list(ax.limits) 

2163 if ax_limits[0] is None: 

2164 ax_limits[0] = -num.inf 

2165 if ax_limits[1] is None: 

2166 ax_limits[1] = num.inf 

2167 in_range = num.logical_and( 

2168 in_range, 

2169 num.logical_and(ax_limits[0] <= x, x <= ax_limits[1])) 

2170 

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

2172 

2173 if not ax.limits or None in ax.limits: 

2174 if len(x) >= 1: 

2175 if in_range is not True: 

2176 xmasked = num.where(in_range, x, num.NaN) 

2177 if percent_interval is None: 

2178 range_this = ( 

2179 num.nanmin(xmasked), 

2180 num.nanmax(xmasked)) 

2181 else: 

2182 xmasked_finite = num.compress( 

2183 num.isfinite(xmasked), xmasked) 

2184 range_this = ( 

2185 scap(xmasked_finite, 

2186 (100.-percent_interval)/2.), 

2187 scap(xmasked_finite, 

2188 100.-(100.-percent_interval)/2.)) 

2189 else: 

2190 if percent_interval is None: 

2191 range_this = num.nanmin(x), num.nanmax(x) 

2192 else: 

2193 xmasked_finite = num.compress( 

2194 num.isfinite(xmasked), xmasked) 

2195 range_this = ( 

2196 scap(xmasked_finite, 

2197 (100.-percent_interval)/2.), 

2198 scap(xmasked_finite, 

2199 100.-(100.-percent_interval)/2.)) 

2200 else: 

2201 range_this = (0., 1.) 

2202 

2203 if ax.limits: 

2204 if ax.limits[0] is not None: 

2205 range_this = ax.limits[0], max(ax.limits[0], 

2206 range_this[1]) 

2207 

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

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

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

2211 

2212 else: 

2213 range_this = ax.limits 

2214 

2215 if data_ranges[i] is None and range_this[0] <= range_this[1]: 

2216 data_ranges[i] = range_this 

2217 else: 

2218 mi, ma = range_this 

2219 if data_ranges[i] is not None: 

2220 mi = min(data_ranges[i][0], mi) 

2221 ma = max(data_ranges[i][1], ma) 

2222 

2223 data_ranges[i] = (mi, ma) 

2224 

2225 for i in range(len(data_ranges)): 

2226 if data_ranges[i] is None or not ( 

2227 num.isfinite(data_ranges[i][0]) 

2228 and num.isfinite(data_ranges[i][1])): 

2229 

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

2231 

2232 self.data_ranges = data_ranges 

2233 self.aspect = aspect 

2234 

2235 def copy(self): 

2236 return ScaleGuru(copy_from=self) 

2237 

2238 def get_params(self, ax_projection=False): 

2239 

2240 ''' 

2241 Get dict with output parameters. 

2242 

2243 For each data dimension, ax minimum, maximum, increment and a label 

2244 string (including unit and exponential factor) are determined. E.g. in 

2245 for the first dimension the output dict will contain the keys 

2246 ``'xmin'``, ``'xmax'``, ``'xinc'``, and ``'xlabel'``. 

2247 

2248 Normally, values corresponding to the scaling of the raw data are 

2249 produced, but if ``ax_projection`` is ``True``, values which are 

2250 suitable to be printed on the axes are returned. This means that in the 

2251 latter case, the :py:attr:`Ax.scaled_unit` and 

2252 :py:attr:`Ax.scaled_unit_factor` attributes as set on the axes are 

2253 respected and that a common 10^x factor is factored out and put to the 

2254 label string. 

2255 ''' 

2256 

2257 xmi, xma, xinc, xlabel = self.axes[0].make_params( 

2258 self.data_ranges[0], ax_projection) 

2259 ymi, yma, yinc, ylabel = self.axes[1].make_params( 

2260 self.data_ranges[1], ax_projection) 

2261 if len(self.axes) > 2: 

2262 zmi, zma, zinc, zlabel = self.axes[2].make_params( 

2263 self.data_ranges[2], ax_projection) 

2264 

2265 # enforce certain aspect, if needed 

2266 if self.aspect is not None: 

2267 xwid = xma-xmi 

2268 ywid = yma-ymi 

2269 if ywid < xwid*self.aspect: 

2270 ymi -= (xwid*self.aspect - ywid)*0.5 

2271 yma += (xwid*self.aspect - ywid)*0.5 

2272 ymi, yma, yinc, ylabel = self.axes[1].make_params( 

2273 (ymi, yma), ax_projection, override_mode='off', 

2274 override_scaled_unit_factor=1.) 

2275 

2276 elif xwid < ywid/self.aspect: 

2277 xmi -= (ywid/self.aspect - xwid)*0.5 

2278 xma += (ywid/self.aspect - xwid)*0.5 

2279 xmi, xma, xinc, xlabel = self.axes[0].make_params( 

2280 (xmi, xma), ax_projection, override_mode='off', 

2281 override_scaled_unit_factor=1.) 

2282 

2283 params = dict(xmin=xmi, xmax=xma, xinc=xinc, xlabel=xlabel, 

2284 ymin=ymi, ymax=yma, yinc=yinc, ylabel=ylabel) 

2285 if len(self.axes) > 2: 

2286 params.update(dict(zmin=zmi, zmax=zma, zinc=zinc, zlabel=zlabel)) 

2287 

2288 return params 

2289 

2290 

2291class GumSpring(object): 

2292 

2293 ''' 

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

2295 ''' 

2296 

2297 def __init__(self, minimal=None, grow=None): 

2298 self.minimal = minimal 

2299 if grow is None: 

2300 if minimal is None: 

2301 self.grow = 1.0 

2302 else: 

2303 self.grow = 0.0 

2304 else: 

2305 self.grow = grow 

2306 self.value = 1.0 

2307 

2308 def get_minimal(self): 

2309 if self.minimal is not None: 

2310 return self.minimal 

2311 else: 

2312 return 0.0 

2313 

2314 def get_grow(self): 

2315 return self.grow 

2316 

2317 def set_value(self, value): 

2318 self.value = value 

2319 

2320 def get_value(self): 

2321 return self.value 

2322 

2323 

2324def distribute(sizes, grows, space): 

2325 sizes = list(sizes) 

2326 gsum = sum(grows) 

2327 if gsum > 0.0: 

2328 for i in range(len(sizes)): 

2329 sizes[i] += space*grows[i]/gsum 

2330 return sizes 

2331 

2332 

2333class Widget(Guru): 

2334 

2335 ''' 

2336 Base class of the gmtpy layout system. 

2337 

2338 The Widget class provides the basic functionality for the nesting and 

2339 placing of elements on the output page, and maintains the sizing policies 

2340 of each element. Each of the layouts defined in gmtpy is itself a Widget. 

2341 

2342 Sizing of the widget is controlled by :py:meth:`get_min_size` and 

2343 :py:meth:`get_grow` which should be overloaded in derived classes. The 

2344 basic behaviour of a Widget instance is to have a vertical and a horizontal 

2345 minimum size which default to zero, as well as a vertical and a horizontal 

2346 desire to grow, represented by floats, which default to 1.0. Additionally 

2347 an aspect ratio constraint may be imposed on the Widget. 

2348 

2349 After layouting, the widget provides its width, height, x-offset and 

2350 y-offset in various ways. Via the Guru interface (see :py:class:`Guru` 

2351 class), templates for the -X, -Y and -J option arguments used by GMT 

2352 arguments are provided. The defaults are suitable for plotting of linear 

2353 (-JX) plots. Other projections can be selected by giving an appropriate 'J' 

2354 template, or by manual construction of the -J option, e.g. by utilizing the 

2355 :py:meth:`width` and :py:meth:`height` methods. The :py:meth:`bbox` method 

2356 can be used to create a PostScript bounding box from the widgets border, 

2357 e.g. for use in the :py:meth:`save` method of :py:class:`GMT` instances. 

2358 

2359 The convention is, that all sizes are given in PostScript points. 

2360 Conversion factors are provided as constants :py:const:`inch` and 

2361 :py:const:`cm` in the gmtpy module. 

2362 ''' 

2363 

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

2365 

2366 ''' 

2367 Create new widget. 

2368 ''' 

2369 

2370 Guru.__init__(self) 

2371 

2372 self.templates = dict( 

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

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

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

2376 

2377 if horizontal is None: 

2378 self.horizontal = GumSpring() 

2379 else: 

2380 self.horizontal = horizontal 

2381 

2382 if vertical is None: 

2383 self.vertical = GumSpring() 

2384 else: 

2385 self.vertical = vertical 

2386 

2387 self.aspect = None 

2388 self.parent = parent 

2389 self.dirty = True 

2390 

2391 def set_parent(self, parent): 

2392 

2393 ''' 

2394 Set the parent widget. 

2395 

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

2397 methods are responsible for calling this. 

2398 ''' 

2399 

2400 self.parent = parent 

2401 self.dirtyfy() 

2402 

2403 def get_parent(self): 

2404 

2405 ''' 

2406 Get the widgets parent widget. 

2407 ''' 

2408 

2409 return self.parent 

2410 

2411 def get_root(self): 

2412 

2413 ''' 

2414 Get the root widget in the layout hierarchy. 

2415 ''' 

2416 

2417 if self.parent is not None: 

2418 return self.get_parent() 

2419 else: 

2420 return self 

2421 

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

2423 

2424 ''' 

2425 Set the horizontal sizing policy of the Widget. 

2426 

2427 

2428 :param minimal: new minimal width of the widget 

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

2430 ''' 

2431 

2432 self.horizontal = GumSpring(minimal, grow) 

2433 self.dirtyfy() 

2434 

2435 def get_horizontal(self): 

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

2437 

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

2439 

2440 ''' 

2441 Set the horizontal sizing policy of the Widget. 

2442 

2443 :param minimal: new minimal height of the widget 

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

2445 ''' 

2446 

2447 self.vertical = GumSpring(minimal, grow) 

2448 self.dirtyfy() 

2449 

2450 def get_vertical(self): 

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

2452 

2453 def set_aspect(self, aspect=None): 

2454 

2455 ''' 

2456 Set aspect constraint on the widget. 

2457 

2458 The aspect is given as height divided by width. 

2459 ''' 

2460 

2461 self.aspect = aspect 

2462 self.dirtyfy() 

2463 

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

2465 

2466 ''' 

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

2468 call. 

2469 ''' 

2470 

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

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

2473 self.set_aspect(aspect) 

2474 

2475 def get_policy(self): 

2476 mh, gh = self.get_horizontal() 

2477 mv, gv = self.get_vertical() 

2478 return (mh, mv), (gh, gv), self.aspect 

2479 

2480 def legalize(self, size, offset): 

2481 

2482 ''' 

2483 Get legal size for widget. 

2484 

2485 Returns: (new_size, new_offset) 

2486 

2487 Given a box as ``size`` and ``offset``, return ``new_size`` and 

2488 ``new_offset``, such that the widget's sizing and aspect constraints 

2489 are fullfilled. The returned box is centered on the given input box. 

2490 ''' 

2491 

2492 sh, sv = size 

2493 oh, ov = offset 

2494 shs, svs = Widget.get_min_size(self) 

2495 ghs, gvs = Widget.get_grow(self) 

2496 

2497 if ghs == 0.0: 

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

2499 sh = shs 

2500 

2501 if gvs == 0.0: 

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

2503 sv = svs 

2504 

2505 if self.aspect is not None: 

2506 if sh > sv/self.aspect: 

2507 oh += (sh-sv/self.aspect)/2. 

2508 sh = sv/self.aspect 

2509 if sv > sh*self.aspect: 

2510 ov += (sv-sh*self.aspect)/2. 

2511 sv = sh*self.aspect 

2512 

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

2514 

2515 def get_min_size(self): 

2516 

2517 ''' 

2518 Get minimum size of widget. 

2519 

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

2521 ''' 

2522 

2523 mh, mv = self.horizontal.get_minimal(), self.vertical.get_minimal() 

2524 if self.aspect is not None: 

2525 if mv == 0.0: 

2526 return mh, mh*self.aspect 

2527 elif mh == 0.0: 

2528 return mv/self.aspect, mv 

2529 return mh, mv 

2530 

2531 def get_grow(self): 

2532 

2533 ''' 

2534 Get widget's desire to grow. 

2535 

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

2537 ''' 

2538 

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

2540 

2541 def set_size(self, size, offset): 

2542 

2543 ''' 

2544 Set the widget's current size. 

2545 

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

2547 responsibility to call this. 

2548 ''' 

2549 

2550 (sh, sv), inner_offset = self.legalize(size, offset) 

2551 self.offset = inner_offset 

2552 self.horizontal.set_value(sh) 

2553 self.vertical.set_value(sv) 

2554 self.dirty = False 

2555 

2556 def __str__(self): 

2557 

2558 def indent(ind, str): 

2559 return ('\n'+ind).join(str.splitlines()) 

2560 size, offset = self.get_size() 

2561 s = "%s (%g x %g) (%g, %g)\n" % ((self.__class__,) + size + offset) 

2562 children = self.get_children() 

2563 if children: 

2564 s += '\n'.join([' ' + indent(' ', str(c)) for c in children]) 

2565 return s 

2566 

2567 def policies_debug_str(self): 

2568 

2569 def indent(ind, str): 

2570 return ('\n'+ind).join(str.splitlines()) 

2571 mins, grows, aspect = self.get_policy() 

2572 s = "%s: minimum=(%s, %s), grow=(%s, %s), aspect=%s\n" % ( 

2573 (self.__class__,) + mins+grows+(aspect,)) 

2574 

2575 children = self.get_children() 

2576 if children: 

2577 s += '\n'.join([' ' + indent( 

2578 ' ', c.policies_debug_str()) for c in children]) 

2579 return s 

2580 

2581 def get_corners(self, descend=False): 

2582 

2583 ''' 

2584 Get coordinates of the corners of the widget. 

2585 

2586 Returns list with coordinate tuples. 

2587 

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

2589 coordinates of all sub-widgets. 

2590 ''' 

2591 

2592 self.do_layout() 

2593 (sh, sv), (oh, ov) = self.get_size() 

2594 corners = [(oh, ov), (oh+sh, ov), (oh+sh, ov+sv), (oh, ov+sv)] 

2595 if descend: 

2596 for child in self.get_children(): 

2597 corners.extend(child.get_corners(descend=True)) 

2598 return corners 

2599 

2600 def get_sizes(self): 

2601 

2602 ''' 

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

2604 

2605 Returns a list with size tuples. 

2606 ''' 

2607 self.do_layout() 

2608 sizes = [self.get_size()] 

2609 for child in self.get_children(): 

2610 sizes.extend(child.get_sizes()) 

2611 return sizes 

2612 

2613 def do_layout(self): 

2614 

2615 ''' 

2616 Triggers layouting of the widget hierarchy, if needed. 

2617 ''' 

2618 

2619 if self.parent is not None: 

2620 return self.parent.do_layout() 

2621 

2622 if not self.dirty: 

2623 return 

2624 

2625 sh, sv = self.get_min_size() 

2626 gh, gv = self.get_grow() 

2627 if sh == 0.0 and gh != 0.0: 

2628 sh = 15.*cm 

2629 if sv == 0.0 and gv != 0.0: 

2630 sv = 15.*cm*gv/gh * 1./golden_ratio 

2631 self.set_size((sh, sv), (0., 0.)) 

2632 

2633 def get_children(self): 

2634 

2635 ''' 

2636 Get sub-widgets contained in this widget. 

2637 

2638 Returns a list of widgets. 

2639 ''' 

2640 

2641 return [] 

2642 

2643 def get_size(self): 

2644 

2645 ''' 

2646 Get current size and position of the widget. 

2647 

2648 Triggers layouting and returns 

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

2650 ''' 

2651 

2652 self.do_layout() 

2653 return (self.horizontal.get_value(), 

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

2655 

2656 def get_params(self): 

2657 

2658 ''' 

2659 Get current size and position of the widget. 

2660 

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

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

2663 ''' 

2664 

2665 self.do_layout() 

2666 (w, h), (xo, yo) = self.get_size() 

2667 return dict(xoffset=xo, yoffset=yo, width=w, height=h, 

2668 width_m=w/_units['m']) 

2669 

2670 def width(self): 

2671 

2672 ''' 

2673 Get current width of the widget. 

2674 

2675 Triggers layouting and returns width. 

2676 ''' 

2677 

2678 self.do_layout() 

2679 return self.horizontal.get_value() 

2680 

2681 def height(self): 

2682 

2683 ''' 

2684 Get current height of the widget. 

2685 

2686 Triggers layouting and return height. 

2687 ''' 

2688 

2689 self.do_layout() 

2690 return self.vertical.get_value() 

2691 

2692 def bbox(self): 

2693 

2694 ''' 

2695 Get PostScript bounding box for this widget. 

2696 

2697 Triggers layouting and returns values suitable to create PS bounding 

2698 box, representing the widgets current size and position. 

2699 ''' 

2700 

2701 self.do_layout() 

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

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

2704 

2705 def dirtyfy(self): 

2706 

2707 ''' 

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

2709 

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

2711 new layouting. 

2712 ''' 

2713 

2714 if self.parent is not None: 

2715 self.parent.dirtyfy() 

2716 

2717 self.dirty = True 

2718 

2719 

2720class CenterLayout(Widget): 

2721 

2722 ''' 

2723 A layout manager which centers its single child widget. 

2724 

2725 The child widget may be oversized. 

2726 ''' 

2727 

2728 def __init__(self, horizontal=None, vertical=None): 

2729 Widget.__init__(self, horizontal, vertical) 

2730 self.content = Widget(horizontal=GumSpring(grow=1.), 

2731 vertical=GumSpring(grow=1.), parent=self) 

2732 

2733 def get_min_size(self): 

2734 shs, svs = Widget.get_min_size(self) 

2735 sh, sv = self.content.get_min_size() 

2736 return max(shs, sh), max(svs, sv) 

2737 

2738 def get_grow(self): 

2739 ghs, gvs = Widget.get_grow(self) 

2740 gh, gv = self.content.get_grow() 

2741 return gh*ghs, gv*gvs 

2742 

2743 def set_size(self, size, offset): 

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

2745 

2746 shc, svc = self.content.get_min_size() 

2747 ghc, gvc = self.content.get_grow() 

2748 if ghc != 0.: 

2749 shc = sh 

2750 if gvc != 0.: 

2751 svc = sv 

2752 ohc = oh+(sh-shc)/2. 

2753 ovc = ov+(sv-svc)/2. 

2754 

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

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

2757 

2758 def set_widget(self, widget=None): 

2759 

2760 ''' 

2761 Set the child widget, which shall be centered. 

2762 ''' 

2763 

2764 if widget is None: 

2765 widget = Widget() 

2766 

2767 self.content = widget 

2768 

2769 widget.set_parent(self) 

2770 

2771 def get_widget(self): 

2772 return self.content 

2773 

2774 def get_children(self): 

2775 return [self.content] 

2776 

2777 

2778class FrameLayout(Widget): 

2779 

2780 ''' 

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

2782 widgets. 

2783 

2784 :: 

2785 

2786 +---------------------------+ 

2787 | top | 

2788 +---------------------------+ 

2789 | | | | 

2790 | left | center | right | 

2791 | | | | 

2792 +---------------------------+ 

2793 | bottom | 

2794 +---------------------------+ 

2795 

2796 This layout manager does a little bit of extra effort to maintain the 

2797 aspect constraint of the center widget, if this is set. It does so, by 

2798 allowing for a bit more flexibility in the sizing of the margins. Two 

2799 shortcut methods are provided to set the margin sizes in one shot: 

2800 :py:meth:`set_fixed_margins` and :py:meth:`set_min_margins`. The first sets 

2801 the margins to fixed sizes, while the second gives them a minimal size and 

2802 a (neglectably) small desire to grow. Using the latter may be useful when 

2803 setting an aspect constraint on the center widget, because this way the 

2804 maximum size of the center widget may be controlled without creating empty 

2805 spaces between the widgets. 

2806 ''' 

2807 

2808 def __init__(self, horizontal=None, vertical=None): 

2809 Widget.__init__(self, horizontal, vertical) 

2810 mw = 3.*cm 

2811 self.left = Widget( 

2812 horizontal=GumSpring(grow=0.15, minimal=mw), parent=self) 

2813 self.right = Widget( 

2814 horizontal=GumSpring(grow=0.15, minimal=mw), parent=self) 

2815 self.top = Widget( 

2816 vertical=GumSpring(grow=0.15, minimal=mw/golden_ratio), 

2817 parent=self) 

2818 self.bottom = Widget( 

2819 vertical=GumSpring(grow=0.15, minimal=mw/golden_ratio), 

2820 parent=self) 

2821 self.center = Widget( 

2822 horizontal=GumSpring(grow=0.7), vertical=GumSpring(grow=0.7), 

2823 parent=self) 

2824 

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

2826 ''' 

2827 Give margins fixed size constraints. 

2828 ''' 

2829 

2830 self.left.set_horizontal(left, 0) 

2831 self.right.set_horizontal(right, 0) 

2832 self.top.set_vertical(top, 0) 

2833 self.bottom.set_vertical(bottom, 0) 

2834 

2835 def set_min_margins(self, left, right, top, bottom, grow=0.0001): 

2836 ''' 

2837 Give margins a minimal size and the possibility to grow. 

2838 

2839 The desire to grow is set to a very small number. 

2840 ''' 

2841 self.left.set_horizontal(left, grow) 

2842 self.right.set_horizontal(right, grow) 

2843 self.top.set_vertical(top, grow) 

2844 self.bottom.set_vertical(bottom, grow) 

2845 

2846 def get_min_size(self): 

2847 shs, svs = Widget.get_min_size(self) 

2848 

2849 sl, sr, st, sb, sc = [x.get_min_size() for x in ( 

2850 self.left, self.right, self.top, self.bottom, self.center)] 

2851 gl, gr, gt, gb, gc = [x.get_grow() for x in ( 

2852 self.left, self.right, self.top, self.bottom, self.center)] 

2853 

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

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

2856 

2857 # prevent widgets from collapsing 

2858 for s, g in ((sl, gl), (sr, gr), (sc, gc)): 

2859 if s[0] == 0.0 and g[0] != 0.0: 

2860 shsum += 0.1*cm 

2861 

2862 for s, g in ((st, gt), (sb, gb), (sc, gc)): 

2863 if s[1] == 0.0 and g[1] != 0.0: 

2864 svsum += 0.1*cm 

2865 

2866 sh = max(shs, shsum) 

2867 sv = max(svs, svsum) 

2868 

2869 return sh, sv 

2870 

2871 def get_grow(self): 

2872 ghs, gvs = Widget.get_grow(self) 

2873 gh = (self.left.get_grow()[0] + 

2874 self.right.get_grow()[0] + 

2875 self.center.get_grow()[0]) * ghs 

2876 gv = (self.top.get_grow()[1] + 

2877 self.bottom.get_grow()[1] + 

2878 self.center.get_grow()[1]) * gvs 

2879 return gh, gv 

2880 

2881 def set_size(self, size, offset): 

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

2883 

2884 sl, sr, st, sb, sc = [x.get_min_size() for x in ( 

2885 self.left, self.right, self.top, self.bottom, self.center)] 

2886 gl, gr, gt, gb, gc = [x.get_grow() for x in ( 

2887 self.left, self.right, self.top, self.bottom, self.center)] 

2888 

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

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

2891 

2892 if ah < 0.0: 

2893 raise GmtPyError("Container not wide enough for contents " 

2894 "(FrameLayout, available: %g cm, needed: %g cm)" 

2895 % (sh/cm, (sl[0]+sr[0]+sc[0])/cm)) 

2896 if av < 0.0: 

2897 raise GmtPyError("Container not high enough for contents " 

2898 "(FrameLayout, available: %g cm, needed: %g cm)" 

2899 % (sv/cm, (st[1]+sb[1]+sc[1])/cm)) 

2900 

2901 slh, srh, sch = distribute((sl[0], sr[0], sc[0]), 

2902 (gl[0], gr[0], gc[0]), ah) 

2903 stv, sbv, scv = distribute((st[1], sb[1], sc[1]), 

2904 (gt[1], gb[1], gc[1]), av) 

2905 

2906 if self.center.aspect is not None: 

2907 ahm = sh - (sl[0]+sr[0] + scv/self.center.aspect) 

2908 avm = sv - (st[1]+sb[1] + sch*self.center.aspect) 

2909 if 0.0 < ahm < ah: 

2910 slh, srh, sch = distribute( 

2911 (sl[0], sr[0], scv/self.center.aspect), 

2912 (gl[0], gr[0], 0.0), ahm) 

2913 

2914 elif 0.0 < avm < av: 

2915 stv, sbv, scv = distribute((st[1], sb[1], 

2916 sch*self.center.aspect), 

2917 (gt[1], gb[1], 0.0), avm) 

2918 

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

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

2921 

2922 oh += ah/2. 

2923 ov += av/2. 

2924 sh -= ah 

2925 sv -= av 

2926 

2927 self.left.set_size((slh, scv), (oh, ov+sbv)) 

2928 self.right.set_size((srh, scv), (oh+slh+sch, ov+sbv)) 

2929 self.top.set_size((sh, stv), (oh, ov+sbv+scv)) 

2930 self.bottom.set_size((sh, sbv), (oh, ov)) 

2931 self.center.set_size((sch, scv), (oh+slh, ov+sbv)) 

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

2933 

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

2935 

2936 ''' 

2937 Set one of the sub-widgets. 

2938 

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

2940 ``'bottom'`` or ``'center'``. 

2941 ''' 

2942 

2943 if widget is None: 

2944 widget = Widget() 

2945 

2946 if which in ('left', 'right', 'top', 'bottom', 'center'): 

2947 self.__dict__[which] = widget 

2948 else: 

2949 raise GmtPyError('No such sub-widget: %s' % which) 

2950 

2951 widget.set_parent(self) 

2952 

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

2954 

2955 ''' 

2956 Get one of the sub-widgets. 

2957 

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

2959 ``'bottom'`` or ``'center'``. 

2960 ''' 

2961 

2962 if which in ('left', 'right', 'top', 'bottom', 'center'): 

2963 return self.__dict__[which] 

2964 else: 

2965 raise GmtPyError('No such sub-widget: %s' % which) 

2966 

2967 def get_children(self): 

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

2969 

2970 

2971class GridLayout(Widget): 

2972 

2973 ''' 

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

2975 

2976 The grid spacing is flexible and based on the sizing policies of the 

2977 contained sub-widgets. If an equidistant grid is needed, the sizing 

2978 policies of the sub-widgets have to be set equally. 

2979 

2980 The height of each row and the width of each column is derived from the 

2981 sizing policy of the largest sub-widget in the row or column in question. 

2982 The algorithm is not very sophisticated, so conflicting sizing policies 

2983 might not be resolved optimally. 

2984 ''' 

2985 

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

2987 

2988 ''' 

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

2990 ''' 

2991 

2992 Widget.__init__(self, horizontal, vertical) 

2993 self.grid = [] 

2994 for iy in range(ny): 

2995 row = [] 

2996 for ix in range(nx): 

2997 w = Widget(parent=self) 

2998 row.append(w) 

2999 

3000 self.grid.append(row) 

3001 

3002 def sub_min_sizes_as_array(self): 

3003 esh = num.array( 

3004 [[w.get_min_size()[0] for w in row] for row in self.grid], 

3005 dtype=float) 

3006 esv = num.array( 

3007 [[w.get_min_size()[1] for w in row] for row in self.grid], 

3008 dtype=float) 

3009 return esh, esv 

3010 

3011 def sub_grows_as_array(self): 

3012 egh = num.array( 

3013 [[w.get_grow()[0] for w in row] for row in self.grid], 

3014 dtype=float) 

3015 egv = num.array( 

3016 [[w.get_grow()[1] for w in row] for row in self.grid], 

3017 dtype=float) 

3018 return egh, egv 

3019 

3020 def get_min_size(self): 

3021 sh, sv = Widget.get_min_size(self) 

3022 esh, esv = self.sub_min_sizes_as_array() 

3023 if esh.size != 0: 

3024 sh = max(sh, num.sum(esh.max(0))) 

3025 if esv.size != 0: 

3026 sv = max(sv, num.sum(esv.max(1))) 

3027 return sh, sv 

3028 

3029 def get_grow(self): 

3030 ghs, gvs = Widget.get_grow(self) 

3031 egh, egv = self.sub_grows_as_array() 

3032 if egh.size != 0: 

3033 gh = num.sum(egh.max(0))*ghs 

3034 else: 

3035 gh = 1.0 

3036 if egv.size != 0: 

3037 gv = num.sum(egv.max(1))*gvs 

3038 else: 

3039 gv = 1.0 

3040 return gh, gv 

3041 

3042 def set_size(self, size, offset): 

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

3044 esh, esv = self.sub_min_sizes_as_array() 

3045 egh, egv = self.sub_grows_as_array() 

3046 

3047 # available additional space 

3048 empty = esh.size == 0 

3049 

3050 if not empty: 

3051 ah = sh - num.sum(esh.max(0)) 

3052 av = sv - num.sum(esv.max(1)) 

3053 else: 

3054 av = sv 

3055 ah = sh 

3056 

3057 if ah < 0.0: 

3058 raise GmtPyError("Container not wide enough for contents " 

3059 "(GridLayout, available: %g cm, needed: %g cm)" 

3060 % (sh/cm, (num.sum(esh.max(0)))/cm)) 

3061 if av < 0.0: 

3062 raise GmtPyError("Container not high enough for contents " 

3063 "(GridLayout, available: %g cm, needed: %g cm)" 

3064 % (sv/cm, (num.sum(esv.max(1)))/cm)) 

3065 

3066 nx, ny = esh.shape 

3067 

3068 if not empty: 

3069 # distribute additional space on rows and columns 

3070 # according to grow weights and minimal sizes 

3071 gsh = egh.sum(1)[:, num.newaxis].repeat(ny, axis=1) 

3072 nesh = esh.copy() 

3073 nesh += num.where(gsh > 0.0, ah*egh/gsh, 0.0) 

3074 

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

3076 

3077 gsv = egv.sum(0)[num.newaxis, :].repeat(nx, axis=0) 

3078 nesv = esv.copy() 

3079 nesv += num.where(gsv > 0.0, av*egv/gsv, 0.0) 

3080 nsv = num.maximum(nesv.max(1), esv.max(1)) 

3081 

3082 ah = sh - sum(nsh) 

3083 av = sv - sum(nsv) 

3084 

3085 oh += ah/2. 

3086 ov += av/2. 

3087 sh -= ah 

3088 sv -= av 

3089 

3090 # resize child widgets 

3091 neov = ov + sum(nsv) 

3092 for row, nesv in zip(self.grid, nsv): 

3093 neov -= nesv 

3094 neoh = oh 

3095 for w, nesh in zip(row, nsh): 

3096 w.set_size((nesh, nesv), (neoh, neov)) 

3097 neoh += nesh 

3098 

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

3100 

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

3102 

3103 ''' 

3104 Set one of the sub-widgets. 

3105 

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

3107 counted from zero. 

3108 ''' 

3109 

3110 if widget is None: 

3111 widget = Widget() 

3112 

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

3114 widget.set_parent(self) 

3115 

3116 def get_widget(self, ix, iy): 

3117 

3118 ''' 

3119 Get one of the sub-widgets. 

3120 

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

3122 counted from zero. 

3123 ''' 

3124 

3125 return self.grid[iy][ix] 

3126 

3127 def get_children(self): 

3128 children = [] 

3129 for row in self.grid: 

3130 children.extend(row) 

3131 

3132 return children 

3133 

3134 

3135def is_gmt5(version='newest'): 

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

3137 

3138 

3139def is_gmt6(version='newest'): 

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

3141 

3142 

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

3144 

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

3146 

3147 if gmt.is_gmt5(): 

3148 gmt.psbasemap('-B+gblack', finish=True, *args, **kwargs) 

3149 fn = gmt.tempfilename('test.eps') 

3150 gmt.save(fn, crop_eps_mode=True) 

3151 with open(fn, 'rb') as f: 

3152 s = f.read() 

3153 

3154 l, b, r, t = get_bbox(s) 

3155 else: 

3156 gmt.psbasemap('-G0', finish=True, *args, **kwargs) 

3157 l, b, r, t = gmt.bbox() 

3158 

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

3160 

3161 

3162def text_box( 

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

3164 

3165 gmt = GMT(version=gmtversion) 

3166 if gmt.is_gmt5(): 

3167 row = [0, 0, text] 

3168 farg = ['-F+f%gp,%s,%s+j%s' % (font_size, font, 'black', 'BL')] 

3169 else: 

3170 row = [0, 0, font_size, 0, font, 'BL', text] 

3171 farg = [] 

3172 

3173 gmt.pstext( 

3174 in_rows=[row], 

3175 finish=True, 

3176 R=(0, 1, 0, 1), 

3177 J='x10p', 

3178 N=True, 

3179 *farg, 

3180 **kwargs) 

3181 

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

3183 gmt.save(fn) 

3184 

3185 (_, stderr) = subprocess.Popen( 

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

3187 stderr=subprocess.PIPE).communicate() 

3188 

3189 dx, dy = None, None 

3190 for line in stderr.splitlines(): 

3191 if line.startswith(b'%%HiResBoundingBox:'): 

3192 l, b, r, t = [float(x) for x in line.split()[-4:]] 

3193 dx, dy = r-l, t-b 

3194 break 

3195 

3196 return dx, dy 

3197 

3198 

3199class TableLiner(object): 

3200 ''' 

3201 Utility class to turn tables into lines. 

3202 ''' 

3203 

3204 def __init__(self, in_columns=None, in_rows=None, encoding='utf-8'): 

3205 self.in_columns = in_columns 

3206 self.in_rows = in_rows 

3207 self.encoding = encoding 

3208 

3209 def __iter__(self): 

3210 if self.in_columns is not None: 

3211 for row in zip(*self.in_columns): 

3212 yield (' '.join([newstr(x) for x in row])+'\n').encode( 

3213 self.encoding) 

3214 

3215 if self.in_rows is not None: 

3216 for row in self.in_rows: 

3217 yield (' '.join([newstr(x) for x in row])+'\n').encode( 

3218 self.encoding) 

3219 

3220 

3221class LineStreamChopper(object): 

3222 ''' 

3223 File-like object to buffer data. 

3224 ''' 

3225 

3226 def __init__(self, liner): 

3227 self.chopsize = None 

3228 self.liner = liner 

3229 self.chop_iterator = None 

3230 self.closed = False 

3231 

3232 def _chopiter(self): 

3233 buf = BytesIO() 

3234 for line in self.liner: 

3235 buf.write(line) 

3236 buflen = buf.tell() 

3237 if self.chopsize is not None and buflen >= self.chopsize: 

3238 buf.seek(0) 

3239 while buf.tell() <= buflen-self.chopsize: 

3240 yield buf.read(self.chopsize) 

3241 

3242 newbuf = BytesIO() 

3243 newbuf.write(buf.read()) 

3244 buf.close() 

3245 buf = newbuf 

3246 

3247 yield(buf.getvalue()) 

3248 buf.close() 

3249 

3250 def read(self, size=None): 

3251 if self.closed: 

3252 raise ValueError('Cannot read from closed LineStreamChopper.') 

3253 if self.chop_iterator is None: 

3254 self.chopsize = size 

3255 self.chop_iterator = self._chopiter() 

3256 

3257 self.chopsize = size 

3258 try: 

3259 return next(self.chop_iterator) 

3260 except StopIteration: 

3261 return '' 

3262 

3263 def close(self): 

3264 self.chopsize = None 

3265 self.chop_iterator = None 

3266 self.closed = True 

3267 

3268 def flush(self): 

3269 pass 

3270 

3271 

3272font_tab = { 

3273 0: 'Helvetica', 

3274 1: 'Helvetica-Bold', 

3275} 

3276 

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

3278 

3279 

3280class GMT(object): 

3281 ''' 

3282 A thin wrapper to GMT command execution. 

3283 

3284 A dict ``config`` may be given to override some of the default GMT 

3285 parameters. The ``version`` argument may be used to select a specific GMT 

3286 version, which should be used with this GMT instance. The selected 

3287 version of GMT has to be installed on the system, must be supported by 

3288 gmtpy and gmtpy must know where to find it. 

3289 

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

3291 output file. 

3292 

3293 Output of a series of GMT commands is accumulated in memory and can then be 

3294 saved as PS or PDF file using the :py:meth:`save` method. 

3295 

3296 GMT commands are accessed as method calls to instances of this class. See 

3297 the :py:meth:`__getattr__` method for details on how the method's 

3298 arguments are translated into options and arguments for the GMT command. 

3299 

3300 Associated with each instance of this class, a temporary directory is 

3301 created, where temporary files may be created, and which is automatically 

3302 deleted, when the object is destroyed. The :py:meth:`tempfilename` method 

3303 may be used to get a random filename in the instance's temporary directory. 

3304 

3305 Any .gmtdefaults files are ignored. The GMT class uses a fixed 

3306 set of defaults, which may be altered via an argument to the constructor. 

3307 If possible, GMT is run in 'isolation mode', which was introduced with GMT 

3308 version 4.2.2, by setting `GMT_TMPDIR` to the instance's temporary 

3309 directory. With earlier versions of GMT, problems may arise with parallel 

3310 execution of more than one GMT instance. 

3311 

3312 Each instance of the GMT class may pick a specific version of GMT which 

3313 shall be used, so that, if multiple versions of GMT are installed on the 

3314 system, different versions of GMT can be used simultaneously such that 

3315 backward compatibility of the scripts can be maintained. 

3316 

3317 ''' 

3318 

3319 def __init__( 

3320 self, 

3321 config=None, 

3322 kontinue=None, 

3323 version='newest', 

3324 config_papersize=None, 

3325 eps_mode=False): 

3326 

3327 self.installation = get_gmt_installation(version) 

3328 self.gmt_config = dict(self.installation['defaults']) 

3329 self.eps_mode = eps_mode 

3330 self._shutil = shutil 

3331 

3332 if config: 

3333 self.gmt_config.update(config) 

3334 

3335 if config_papersize: 

3336 if not isinstance(config_papersize, str): 

3337 config_papersize = 'Custom_%ix%i' % ( 

3338 int(config_papersize[0]), int(config_papersize[1])) 

3339 

3340 if self.is_gmt5(): 

3341 self.gmt_config['PS_MEDIA'] = config_papersize 

3342 else: 

3343 self.gmt_config['PAPER_MEDIA'] = config_papersize 

3344 

3345 self.tempdir = tempfile.mkdtemp("", "gmtpy-") 

3346 self.gmt_config_filename = pjoin(self.tempdir, 'gmt.conf') 

3347 self.gen_gmt_config_file(self.gmt_config_filename, self.gmt_config) 

3348 

3349 if kontinue is not None: 

3350 self.load_unfinished(kontinue) 

3351 self.needstart = False 

3352 else: 

3353 self.output = BytesIO() 

3354 self.needstart = True 

3355 

3356 self.finished = False 

3357 

3358 self.environ = os.environ.copy() 

3359 self.environ['GMTHOME'] = self.installation.get('home', '') 

3360 # GMT isolation mode: works only properly with GMT version >= 4.2.2 

3361 self.environ['GMT_TMPDIR'] = self.tempdir 

3362 

3363 self.layout = None 

3364 self.command_log = [] 

3365 self.keep_temp_dir = False 

3366 

3367 def is_gmt5(self): 

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

3369 

3370 def is_gmt6(self): 

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

3372 

3373 def get_version(self): 

3374 return self.installation['version'] 

3375 

3376 def get_config(self, key): 

3377 return self.gmt_config[key] 

3378 

3379 def to_points(self, string): 

3380 if not string: 

3381 return 0 

3382 

3383 unit = string[-1] 

3384 if unit in _units: 

3385 return float(string[:-1])/_units[unit] 

3386 else: 

3387 default_unit = measure_unit(self.gmt_config).lower()[0] 

3388 return float(string)/_units[default_unit] 

3389 

3390 def label_font_size(self): 

3391 if self.is_gmt5(): 

3392 return self.to_points(self.gmt_config['FONT_LABEL'].split(',')[0]) 

3393 else: 

3394 return self.to_points(self.gmt_config['LABEL_FONT_SIZE']) 

3395 

3396 def label_font(self): 

3397 if self.is_gmt5(): 

3398 return font_tab_rev(self.gmt_config['FONT_LABEL'].split(',')[1]) 

3399 else: 

3400 return self.gmt_config['LABEL_FONT'] 

3401 

3402 def gen_gmt_config_file(self, config_filename, config): 

3403 f = open(config_filename, 'wb') 

3404 f.write( 

3405 ('#\n# GMT %s Defaults file\n' 

3406 % self.installation['version']).encode('ascii')) 

3407 

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

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

3410 f.close() 

3411 

3412 def __del__(self): 

3413 if not self.keep_temp_dir: 

3414 self._shutil.rmtree(self.tempdir) 

3415 

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

3417 

3418 ''' 

3419 Execute arbitrary GMT command. 

3420 

3421 See docstring in __getattr__ for details. 

3422 ''' 

3423 

3424 in_stream = kwargs.pop('in_stream', None) 

3425 in_filename = kwargs.pop('in_filename', None) 

3426 in_string = kwargs.pop('in_string', None) 

3427 in_columns = kwargs.pop('in_columns', None) 

3428 in_rows = kwargs.pop('in_rows', None) 

3429 out_stream = kwargs.pop('out_stream', None) 

3430 out_filename = kwargs.pop('out_filename', None) 

3431 out_discard = kwargs.pop('out_discard', None) 

3432 finish = kwargs.pop('finish', False) 

3433 suppressdefaults = kwargs.pop('suppress_defaults', False) 

3434 config_override = kwargs.pop('config', None) 

3435 

3436 assert(not self.finished) 

3437 

3438 # check for mutual exclusiveness on input and output possibilities 

3439 assert(1 >= len( 

3440 [x for x in [ 

3441 in_stream, in_filename, in_string, in_columns, in_rows] 

3442 if x is not None])) 

3443 assert(1 >= len([x for x in [out_stream, out_filename, out_discard] 

3444 if x is not None])) 

3445 

3446 options = [] 

3447 

3448 gmt_config = self.gmt_config 

3449 if not self.is_gmt5(): 

3450 gmt_config_filename = self.gmt_config_filename 

3451 if config_override: 

3452 gmt_config = self.gmt_config.copy() 

3453 gmt_config.update(config_override) 

3454 gmt_config_override_filename = pjoin( 

3455 self.tempdir, 'gmtdefaults_override') 

3456 self.gen_gmt_config_file( 

3457 gmt_config_override_filename, gmt_config) 

3458 gmt_config_filename = gmt_config_override_filename 

3459 

3460 else: # gmt5 needs override variables as --VAR=value 

3461 if config_override: 

3462 for k, v in config_override.items(): 

3463 options.append('--%s=%s' % (k, v)) 

3464 

3465 if out_discard: 

3466 out_filename = '/dev/null' 

3467 

3468 out_mustclose = False 

3469 if out_filename is not None: 

3470 out_mustclose = True 

3471 out_stream = open(out_filename, 'wb') 

3472 

3473 if in_filename is not None: 

3474 in_stream = open(in_filename, 'rb') 

3475 

3476 if in_string is not None: 

3477 in_stream = BytesIO(in_string) 

3478 

3479 encoding_gmt = gmt_config.get( 

3480 'PS_CHAR_ENCODING', 

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

3482 

3483 encoding = encoding_gmt_to_python[encoding_gmt.lower()] 

3484 

3485 if in_columns is not None or in_rows is not None: 

3486 in_stream = LineStreamChopper(TableLiner(in_columns=in_columns, 

3487 in_rows=in_rows, 

3488 encoding=encoding)) 

3489 

3490 # convert option arguments to strings 

3491 for k, v in kwargs.items(): 

3492 if len(k) > 1: 

3493 raise GmtPyError('Found illegal keyword argument "%s" ' 

3494 'while preparing options for command "%s"' 

3495 % (k, command)) 

3496 

3497 if type(v) is bool: 

3498 if v: 

3499 options.append('-%s' % k) 

3500 elif type(v) is tuple or type(v) is list: 

3501 options.append('-%s' % k + '/'.join([str(x) for x in v])) 

3502 else: 

3503 options.append('-%s%s' % (k, str(v))) 

3504 

3505 # if not redirecting to an external sink, handle -K -O 

3506 if out_stream is None: 

3507 if not finish: 

3508 options.append('-K') 

3509 else: 

3510 self.finished = True 

3511 

3512 if not self.needstart: 

3513 options.append('-O') 

3514 else: 

3515 self.needstart = False 

3516 

3517 out_stream = self.output 

3518 

3519 # run the command 

3520 if self.is_gmt5(): 

3521 args = [pjoin(self.installation['bin'], 'gmt'), command] 

3522 else: 

3523 args = [pjoin(self.installation['bin'], command)] 

3524 

3525 if not os.path.isfile(args[0]): 

3526 raise OSError('No such file: %s' % args[0]) 

3527 args.extend(options) 

3528 args.extend(addargs) 

3529 if not self.is_gmt5() and not suppressdefaults: 

3530 # does not seem to work with GMT 5 (and should not be necessary 

3531 args.append('+'+gmt_config_filename) 

3532 

3533 bs = 2048 

3534 p = subprocess.Popen(args, stdin=subprocess.PIPE, 

3535 stdout=subprocess.PIPE, bufsize=bs, 

3536 env=self.environ) 

3537 while True: 

3538 cr, cw, cx = select([p.stdout], [p.stdin], []) 

3539 if cr: 

3540 out_stream.write(p.stdout.read(bs)) 

3541 if cw: 

3542 if in_stream is not None: 

3543 data = in_stream.read(bs) 

3544 if len(data) == 0: 

3545 break 

3546 p.stdin.write(data) 

3547 else: 

3548 break 

3549 if not cr and not cw: 

3550 break 

3551 

3552 p.stdin.close() 

3553 

3554 while True: 

3555 data = p.stdout.read(bs) 

3556 if len(data) == 0: 

3557 break 

3558 out_stream.write(data) 

3559 

3560 p.stdout.close() 

3561 

3562 retcode = p.wait() 

3563 

3564 if in_stream is not None: 

3565 in_stream.close() 

3566 

3567 if out_mustclose: 

3568 out_stream.close() 

3569 

3570 if retcode != 0: 

3571 self.keep_temp_dir = True 

3572 raise GMTError('Command %s returned an error. ' 

3573 'While executing command:\n%s' 

3574 % (command, escape_shell_args(args))) 

3575 

3576 self.command_log.append(args) 

3577 

3578 def __getattr__(self, command): 

3579 

3580 ''' 

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

3582 

3583 Execute arbitrary GMT command. 

3584 

3585 Run a GMT command and by default append its postscript output to the 

3586 output file maintained by the GMT instance on which this method is 

3587 called. 

3588 

3589 Except for a few keyword arguments listed below, any ``kwargs`` and 

3590 ``addargs`` are converted into command line options and arguments and 

3591 passed to the GMT command. Numbers in keyword arguments are converted 

3592 into strings. E.g. ``S=10`` is translated into ``'-S10'``. Tuples of 

3593 numbers or strings are converted into strings where the elements of the 

3594 tuples are separated by slashes '/'. E.g. ``R=(10, 10, 20, 20)`` is 

3595 translated into ``'-R10/10/20/20'``. Options with a boolean argument 

3596 are only appended to the GMT command, if their values are True. 

3597 

3598 If no output redirection is in effect, the -K and -O options are 

3599 handled by gmtpy and thus should not be specified. Use 

3600 ``out_discard=True`` if you don't want -K or -O beeing added, but are 

3601 not interested in the output. 

3602 

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

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

3605 

3606 =============== ======================================================= 

3607 ``in_stream`` Data is read from an open file like object. 

3608 ``in_filename`` Data is read from the given file. 

3609 ``in_string`` String content is dumped to the process. 

3610 ``in_columns`` A 2D nested iterable whose elements can be accessed as 

3611 ``in_columns[icolumn][irow]`` is converted into an 

3612 ascii 

3613 table, which is fed to the process. 

3614 ``in_rows`` A 2D nested iterable whos elements can be accessed as 

3615 ``in_rows[irow][icolumn]`` is converted into an ascii 

3616 table, which is fed to the process. 

3617 =============== ======================================================= 

3618 

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

3620 following options: 

3621 

3622 ================= ===================================================== 

3623 ``out_stream`` Output is fed to an open file like object. 

3624 ``out_filename`` Output is dumped to the given file. 

3625 ``out_discard`` If True, output is dumped to :file:`/dev/null`. 

3626 ================= ===================================================== 

3627 

3628 Additional keyword arguments: 

3629 

3630 ===================== ================================================= 

3631 ``config`` Dict with GMT defaults which override the 

3632 currently active set of defaults exclusively 

3633 during this call. 

3634 ``finish`` If True, the postscript file, which is maintained 

3635 by the GMT instance is finished, and no further 

3636 plotting is allowed. 

3637 ``suppress_defaults`` Suppress appending of the ``'+gmtdefaults'`` 

3638 option to the command. 

3639 ===================== ================================================= 

3640 

3641 ''' 

3642 

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

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

3645 return f 

3646 

3647 def tempfilename(self, name=None): 

3648 ''' 

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

3650 

3651 If no ``name`` argument is given, a random name is picked. If 

3652 ``name`` is given, returns a path ending in that ``name``. 

3653 ''' 

3654 

3655 if not name: 

3656 name = ''.join( 

3657 [random.choice('abcdefghijklmnopqrstuvwxyz') 

3658 for i in range(10)]) 

3659 

3660 fn = pjoin(self.tempdir, name) 

3661 return fn 

3662 

3663 def tempfile(self, name=None): 

3664 ''' 

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

3666 ''' 

3667 

3668 fn = self.tempfilename(name) 

3669 f = open(fn, 'wb') 

3670 return f, fn 

3671 

3672 def save_unfinished(self, filename): 

3673 out = open(filename, 'wb') 

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

3675 out.close() 

3676 

3677 def load_unfinished(self, filename): 

3678 self.output = BytesIO() 

3679 self.finished = False 

3680 inp = open(filename, 'rb') 

3681 self.output.write(inp.read()) 

3682 inp.close() 

3683 

3684 def dump(self, ident): 

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

3686 self.save_unfinished(filename) 

3687 

3688 def load(self, ident): 

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

3690 self.load_unfinished(filename) 

3691 

3692 def save(self, filename=None, bbox=None, resolution=150, oversample=2., 

3693 width=None, height=None, size=None, crop_eps_mode=False, 

3694 psconvert=False): 

3695 

3696 ''' 

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

3698 

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

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

3701 

3702 If filename ends with ``'.png'`` a PNG file is created by running 

3703 :program:`gmtpy-epstopdf`, :program:`pdftocairo` and 

3704 :program:`convert`. ``resolution`` specifies the resolution in DPI for 

3705 raster file formats. Rasterization is done at a higher resolution if 

3706 ``oversample`` is set to a value higher than one. The output image size 

3707 can also be controlled by setting ``width``, ``height`` or ``size`` 

3708 instead of ``resolution``. When ``size`` is given, the image is scaled 

3709 so that ``max(width, height) == size``. 

3710 

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

3712 ''' 

3713 

3714 if not self.finished: 

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

3716 

3717 if filename: 

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

3719 out = open(tempfn, 'wb') 

3720 else: 

3721 out = sys.stdout 

3722 

3723 if bbox and not self.is_gmt5(): 

3724 out.write(replace_bbox(bbox, self.output.getvalue())) 

3725 else: 

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

3727 

3728 if filename: 

3729 out.close() 

3730 

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

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

3733 

3734 shutil.move(tempfn, filename) 

3735 return 

3736 

3737 if self.is_gmt5(): 

3738 if crop_eps_mode: 

3739 addarg = ['-A'] 

3740 else: 

3741 addarg = [] 

3742 

3743 subprocess.call( 

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

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

3746 

3747 if bbox: 

3748 with open(tempfn + '.eps', 'rb') as fin: 

3749 with open(tempfn + '-fixbb.eps', 'wb') as fout: 

3750 replace_bbox(bbox, fin, fout) 

3751 

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

3753 

3754 else: 

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

3756 

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

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

3759 return 

3760 

3761 elif filename.endswith('.pdf'): 

3762 if psconvert: 

3763 gmt_bin = pjoin(self.installation['bin'], 'gmt') 

3764 subprocess.call([gmt_bin, 'psconvert', tempfn + '.eps', '-Tf', 

3765 '-F' + filename]) 

3766 else: 

3767 subprocess.call(['gmtpy-epstopdf', '--res=%i' % resolution, 

3768 '--outfile=' + filename, tempfn + '.eps']) 

3769 else: 

3770 subprocess.call([ 

3771 'gmtpy-epstopdf', 

3772 '--res=%i' % (resolution * oversample), 

3773 '--outfile=' + tempfn + '.pdf', tempfn + '.eps']) 

3774 

3775 convert_graph( 

3776 tempfn + '.pdf', filename, 

3777 resolution=resolution, oversample=oversample, 

3778 size=size, width=width, height=height) 

3779 

3780 def bbox(self): 

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

3782 

3783 def get_command_log(self): 

3784 ''' 

3785 Get the command log. 

3786 ''' 

3787 

3788 return self.command_log 

3789 

3790 def __str__(self): 

3791 s = '' 

3792 for com in self.command_log: 

3793 s += com[0] + "\n " + "\n ".join(com[1:]) + "\n\n" 

3794 return s 

3795 

3796 def page_size_points(self): 

3797 ''' 

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

3799 ''' 

3800 

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

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

3803 pm = pm[:-1] 

3804 

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

3806 

3807 if pm in all_paper_sizes(): 

3808 

3809 if orient == 'portrait': 

3810 return get_paper_size(pm) 

3811 else: 

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

3813 

3814 m = re.match(r'custom_([0-9.]+)([cimp]?)x([0-9.]+)([cimp]?)', pm) 

3815 if m: 

3816 w, uw, h, uh = m.groups() 

3817 w, h = float(w), float(h) 

3818 if uw: 

3819 w *= _units[uw] 

3820 if uh: 

3821 h *= _units[uh] 

3822 if orient == 'portrait': 

3823 return w, h 

3824 else: 

3825 return h, w 

3826 

3827 return None, None 

3828 

3829 def default_layout(self, with_palette=False): 

3830 ''' 

3831 Get a default layout for the output page. 

3832 

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

3834 `PAPER_MEDIA` setting in the GMT configuration dict. 

3835 

3836 If `PAPER_MEDIA` ends with a ``'+'`` (EPS output is selected), a 

3837 :py:class:`FrameLayout` is centered on the page, whose size is 

3838 controlled by its center widget's size plus the margins of the 

3839 :py:class:`FrameLayout`. 

3840 

3841 If `PAPER_MEDIA` indicates, that a custom page size is wanted by 

3842 starting with ``'Custom_'``, a :py:class:`FrameLayout` is used to fill 

3843 the complete page. The center widget's size is then controlled by the 

3844 page's size minus the margins of the :py:class:`FrameLayout`. 

3845 

3846 In any other case, two FrameLayouts are nested, such that the outer 

3847 layout attaches a 1 cm (printer) margin around the complete page, and 

3848 the inner FrameLayout's center widget takes up as much space as 

3849 possible under the constraint, that an aspect ratio of 1/golden_ratio 

3850 is preserved. 

3851 

3852 In any case, a reference to the innermost :py:class:`FrameLayout` 

3853 instance is returned. The top-level layout can be accessed by calling 

3854 :py:meth:`Widget.get_parent` on the returned layout. 

3855 ''' 

3856 

3857 if self.layout is None: 

3858 w, h = self.page_size_points() 

3859 

3860 if w is None or h is None: 

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

3862 

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

3864 

3865 if with_palette: 

3866 palette_layout = GridLayout(3, 1) 

3867 spacer = palette_layout.get_widget(1, 0) 

3868 palette_widget = palette_layout.get_widget(2, 0) 

3869 spacer.set_horizontal(0.5*cm) 

3870 palette_widget.set_horizontal(0.5*cm) 

3871 

3872 if pm.endswith('+') or self.eps_mode: 

3873 outer = CenterLayout() 

3874 outer.set_policy((w, h), (0., 0.)) 

3875 inner = FrameLayout() 

3876 outer.set_widget(inner) 

3877 if with_palette: 

3878 inner.set_widget('center', palette_layout) 

3879 widget = palette_layout 

3880 else: 

3881 widget = inner.get_widget('center') 

3882 widget.set_policy((w/golden_ratio, 0.), (0., 0.), 

3883 aspect=1./golden_ratio) 

3884 mw = 3.0*cm 

3885 inner.set_fixed_margins( 

3886 mw, mw, mw/golden_ratio, mw/golden_ratio) 

3887 self.layout = inner 

3888 

3889 elif pm.startswith('custom_'): 

3890 layout = FrameLayout() 

3891 layout.set_policy((w, h), (0., 0.)) 

3892 mw = 3.0*cm 

3893 layout.set_min_margins( 

3894 mw, mw, mw/golden_ratio, mw/golden_ratio) 

3895 if with_palette: 

3896 layout.set_widget('center', palette_layout) 

3897 self.layout = layout 

3898 else: 

3899 outer = FrameLayout() 

3900 outer.set_policy((w, h), (0., 0.)) 

3901 outer.set_fixed_margins(1.*cm, 1.*cm, 1.*cm, 1.*cm) 

3902 

3903 inner = FrameLayout() 

3904 outer.set_widget('center', inner) 

3905 mw = 3.0*cm 

3906 inner.set_min_margins(mw, mw, mw/golden_ratio, mw/golden_ratio) 

3907 if with_palette: 

3908 inner.set_widget('center', palette_layout) 

3909 widget = palette_layout 

3910 else: 

3911 widget = inner.get_widget('center') 

3912 

3913 widget.set_aspect(1./golden_ratio) 

3914 

3915 self.layout = inner 

3916 

3917 return self.layout 

3918 

3919 def draw_layout(self, layout): 

3920 ''' 

3921 Use psxy to draw layout; for debugging 

3922 ''' 

3923 

3924 # corners = layout.get_corners(descend=True) 

3925 rects = num.array(layout.get_sizes(), dtype=float) 

3926 rects_wid = rects[:, 0, 0] 

3927 rects_hei = rects[:, 0, 1] 

3928 rects_center_x = rects[:, 1, 0] + rects_wid*0.5 

3929 rects_center_y = rects[:, 1, 1] + rects_hei*0.5 

3930 nrects = len(rects) 

3931 prects = (rects_center_x, rects_center_y, num.arange(nrects), 

3932 num.zeros(nrects), rects_hei, rects_wid) 

3933 

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

3935 

3936 cptfile = self.tempfilename() + '.cpt' 

3937 self.makecpt( 

3938 C='ocean', 

3939 T='%g/%g/%g' % (-nrects, nrects, 1), 

3940 Z=True, 

3941 out_filename=cptfile, suppress_defaults=True) 

3942 

3943 bb = layout.bbox() 

3944 self.psxy( 

3945 in_columns=prects, 

3946 C=cptfile, 

3947 W='1p', 

3948 S='J', 

3949 R=(bb[0], bb[2], bb[1], bb[3]), 

3950 *layout.XYJ()) 

3951 

3952 

3953def simpleconf_to_ax(conf, axname): 

3954 c = {} 

3955 x = axname 

3956 for x in ('', axname): 

3957 for k in ('label', 'unit', 'scaled_unit', 'scaled_unit_factor', 

3958 'space', 'mode', 'approx_ticks', 'limits', 'masking', 'inc', 

3959 'snap'): 

3960 

3961 if x+k in conf: 

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

3963 

3964 return Ax(**c) 

3965 

3966 

3967class DensityPlotDef(object): 

3968 def __init__(self, data, cpt='ocean', tension=0.7, size=(640, 480), 

3969 contour=False, method='surface', zscaler=None, **extra): 

3970 self.data = data 

3971 self.cpt = cpt 

3972 self.tension = tension 

3973 self.size = size 

3974 self.contour = contour 

3975 self.method = method 

3976 self.zscaler = zscaler 

3977 self.extra = extra 

3978 

3979 

3980class TextDef(object): 

3981 def __init__( 

3982 self, 

3983 data, 

3984 size=9, 

3985 justify='MC', 

3986 fontno=0, 

3987 offset=(0, 0), 

3988 color='black'): 

3989 

3990 self.data = data 

3991 self.size = size 

3992 self.justify = justify 

3993 self.fontno = fontno 

3994 self.offset = offset 

3995 self.color = color 

3996 

3997 

3998class Simple(object): 

3999 def __init__(self, gmtconfig=None, gmtversion='newest', **simple_config): 

4000 self.data = [] 

4001 self.symbols = [] 

4002 self.config = copy.deepcopy(simple_config) 

4003 self.gmtconfig = gmtconfig 

4004 self.density_plot_defs = [] 

4005 self.text_defs = [] 

4006 

4007 self.gmtversion = gmtversion 

4008 

4009 self.data_x = [] 

4010 self.symbols_x = [] 

4011 

4012 self.data_y = [] 

4013 self.symbols_y = [] 

4014 

4015 self.default_config = {} 

4016 self.set_defaults(width=15.*cm, 

4017 height=15.*cm / golden_ratio, 

4018 margins=(2.*cm, 2.*cm, 2.*cm, 2.*cm), 

4019 with_palette=False, 

4020 palette_offset=0.5*cm, 

4021 palette_width=None, 

4022 palette_height=None, 

4023 zlabeloffset=2*cm, 

4024 draw_layout=False) 

4025 

4026 self.setup_defaults() 

4027 self.fixate_widget_aspect = False 

4028 

4029 def setup_defaults(self): 

4030 pass 

4031 

4032 def set_defaults(self, **kwargs): 

4033 self.default_config.update(kwargs) 

4034 

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

4036 self.data.append(data) 

4037 self.symbols.append(symbol) 

4038 

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

4040 dpd = DensityPlotDef(data, **kwargs) 

4041 self.density_plot_defs.append(dpd) 

4042 

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

4044 dpd = TextDef(data, **kwargs) 

4045 self.text_defs.append(dpd) 

4046 

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

4048 self.data_x.append(data) 

4049 self.symbols_x.append(symbol) 

4050 

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

4052 self.data_y.append(data) 

4053 self.symbols_y.append(symbol) 

4054 

4055 def set(self, **kwargs): 

4056 self.config.update(kwargs) 

4057 

4058 def setup_base(self, conf): 

4059 w = conf.pop('width') 

4060 h = conf.pop('height') 

4061 margins = conf.pop('margins') 

4062 

4063 gmtconfig = {} 

4064 if self.gmtconfig is not None: 

4065 gmtconfig.update(self.gmtconfig) 

4066 

4067 gmt = GMT( 

4068 version=self.gmtversion, 

4069 config=gmtconfig, 

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

4071 

4072 layout = gmt.default_layout(with_palette=conf['with_palette']) 

4073 layout.set_min_margins(*margins) 

4074 if conf['with_palette']: 

4075 widget = layout.get_widget().get_widget(0, 0) 

4076 spacer = layout.get_widget().get_widget(1, 0) 

4077 spacer.set_horizontal(conf['palette_offset']) 

4078 palette_widget = layout.get_widget().get_widget(2, 0) 

4079 if conf['palette_width'] is not None: 

4080 palette_widget.set_horizontal(conf['palette_width']) 

4081 if conf['palette_height'] is not None: 

4082 palette_widget.set_vertical(conf['palette_height']) 

4083 widget.set_vertical(h-margins[2]-margins[3]-0.03*cm) 

4084 return gmt, layout, widget, palette_widget 

4085 else: 

4086 widget = layout.get_widget() 

4087 return gmt, layout, widget, None 

4088 

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

4090 pass 

4091 

4092 def setup_scaling(self, conf): 

4093 ndims = 2 

4094 if self.density_plot_defs: 

4095 ndims = 3 

4096 

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

4098 

4099 data_all = [] 

4100 data_all.extend(self.data) 

4101 for dsd in self.density_plot_defs: 

4102 if dsd.zscaler is None: 

4103 data_all.append(dsd.data) 

4104 else: 

4105 data_all.append(dsd.data[:2]) 

4106 data_chopped = [ds[:ndims] for ds in data_all] 

4107 

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

4109 

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

4111 

4112 return scaler 

4113 

4114 def setup_scaling_plus(self, scaler, axes): 

4115 pass 

4116 

4117 def setup_scaling_extra(self, scaler, conf): 

4118 

4119 scaler_x = scaler.copy() 

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

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

4122 

4123 scaler_y = scaler.copy() 

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

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

4126 

4127 return scaler_x, scaler_y 

4128 

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

4130 

4131 R = scaler.R() 

4132 # par = scaler.get_params() 

4133 rxyj = R + widget.XYJ() 

4134 innerticks = False 

4135 for dpd in self.density_plot_defs: 

4136 

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

4138 

4139 if dpd.zscaler is not None: 

4140 s = dpd.zscaler 

4141 else: 

4142 s = scaler 

4143 

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

4145 

4146 fn_grid = gmt.tempfilename() 

4147 

4148 fn_mean = gmt.tempfilename() 

4149 

4150 if dpd.method in ('surface', 'triangulate'): 

4151 gmt.blockmean(in_columns=dpd.data, 

4152 I='%i+/%i+' % dpd.size, # noqa 

4153 out_filename=fn_mean, *R) 

4154 

4155 if dpd.method == 'surface': 

4156 gmt.surface( 

4157 in_filename=fn_mean, 

4158 T=dpd.tension, 

4159 G=fn_grid, 

4160 I='%i+/%i+' % dpd.size, # noqa 

4161 out_discard=True, 

4162 *R) 

4163 

4164 if dpd.method == 'triangulate': 

4165 gmt.triangulate( 

4166 in_filename=fn_mean, 

4167 G=fn_grid, 

4168 I='%i+/%i+' % dpd.size, # noqa 

4169 out_discard=True, 

4170 V=True, 

4171 *R) 

4172 

4173 if gmt.is_gmt5(): 

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

4175 

4176 else: 

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

4178 

4179 if dpd.contour: 

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

4181 innerticks = '0.5p,black' 

4182 

4183 os.remove(fn_grid) 

4184 os.remove(fn_mean) 

4185 

4186 if dpd.method == 'fillcontour': 

4187 extra = dict(C=fn_cpt) 

4188 extra.update(dpd.extra) 

4189 gmt.pscontour(in_columns=dpd.data, 

4190 I=True, *rxyj, **extra) # noqa 

4191 

4192 if dpd.method == 'contour': 

4193 extra = dict(W='0.5p,black', C=fn_cpt) 

4194 extra.update(dpd.extra) 

4195 gmt.pscontour(in_columns=dpd.data, *rxyj, **extra) 

4196 

4197 return fn_cpt, innerticks 

4198 

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

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

4201 

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

4203 rxyj = scaler.R() + widget.JXY() 

4204 for dat, sym in zip(self.data, self.symbols): 

4205 gmt.psxy(in_columns=dat, *(sym.split()+rxyj)) 

4206 

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

4208 pass 

4209 

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

4211 pass 

4212 

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

4214 

4215 for dat, sym in zip(self.data_x, self.symbols_x): 

4216 gmt.psxy(in_columns=dat, 

4217 *(sym.split() + scaler_x.R() + widget.JXY())) 

4218 

4219 for dat, sym in zip(self.data_y, self.symbols_y): 

4220 gmt.psxy(in_columns=dat, 

4221 *(sym.split() + scaler_y.R() + widget.JXY())) 

4222 

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

4224 

4225 rxyj = scaler.R() + widget.JXY() 

4226 for td in self.text_defs: 

4227 x, y = td.data[0:2] 

4228 text = td.data[-1] 

4229 size = td.size 

4230 angle = 0 

4231 fontno = td.fontno 

4232 justify = td.justify 

4233 color = td.color 

4234 if gmt.is_gmt5(): 

4235 gmt.pstext( 

4236 in_rows=[(x, y, text)], 

4237 F='+f%gp,%s,%s+a%g+j%s' % ( 

4238 size, fontno, color, angle, justify), 

4239 D='%gp/%gp' % td.offset, *rxyj) 

4240 else: 

4241 gmt.pstext( 

4242 in_rows=[(x, y, size, angle, fontno, justify, text)], 

4243 D='%gp/%gp' % td.offset, *rxyj) 

4244 

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

4246 

4247 conf = dict(self.default_config) 

4248 conf.update(self.config) 

4249 

4250 gmt, layout, widget, palette_widget = self.setup_base(conf) 

4251 scaler = self.setup_scaling(conf) 

4252 scaler_x, scaler_y = self.setup_scaling_extra(scaler, conf) 

4253 

4254 self.setup_projection(widget, scaler, conf) 

4255 if self.fixate_widget_aspect: 

4256 aspect = aspect_for_projection( 

4257 gmt.installation['version'], *(widget.J() + scaler.R())) 

4258 

4259 widget.set_aspect(aspect) 

4260 

4261 if conf['draw_layout']: 

4262 gmt.draw_layout(layout) 

4263 cptfile = None 

4264 if self.density_plot_defs: 

4265 cptfile, innerticks = self.draw_density(gmt, widget, scaler) 

4266 self.pre_draw(gmt, widget, scaler) 

4267 self.draw(gmt, widget, scaler) 

4268 self.post_draw(gmt, widget, scaler) 

4269 self.draw_extra(gmt, widget, scaler_x, scaler_y) 

4270 self.draw_text(gmt, widget, scaler) 

4271 self.draw_basemap(gmt, widget, scaler) 

4272 

4273 if palette_widget and cptfile: 

4274 nice_palette(gmt, palette_widget, scaler, cptfile, 

4275 innerticks=innerticks, 

4276 zlabeloffset=conf['zlabeloffset']) 

4277 

4278 gmt.save(filename, resolution=resolution) 

4279 

4280 

4281class LinLinPlot(Simple): 

4282 pass 

4283 

4284 

4285class LogLinPlot(Simple): 

4286 

4287 def setup_defaults(self): 

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

4289 

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

4291 widget['J'] = '-JX%(width)gpl/%(height)gp' 

4292 scaler['B'] = '-B2:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen' 

4293 

4294 

4295class LinLogPlot(Simple): 

4296 

4297 def setup_defaults(self): 

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

4299 

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

4301 widget['J'] = '-JX%(width)gp/%(height)gpl' 

4302 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/2:%(ylabel)s:WSen' 

4303 

4304 

4305class LogLogPlot(Simple): 

4306 

4307 def setup_defaults(self): 

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

4309 

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

4311 widget['J'] = '-JX%(width)gpl/%(height)gpl' 

4312 scaler['B'] = '-B2:%(xlabel)s:/2:%(ylabel)s:WSen' 

4313 

4314 

4315class AziDistPlot(Simple): 

4316 

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

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

4319 self.fixate_widget_aspect = True 

4320 

4321 def setup_defaults(self): 

4322 self.set_defaults( 

4323 height=15.*cm, 

4324 width=15.*cm, 

4325 xmode='off', 

4326 xlimits=(0., 360.), 

4327 xinc=45.) 

4328 

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

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

4331 

4332 def setup_scaling_plus(self, scaler, axes): 

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

4334 

4335 

4336class MPlot(Simple): 

4337 

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

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

4340 self.fixate_widget_aspect = True 

4341 

4342 def setup_defaults(self): 

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

4344 

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

4346 par = scaler.get_params() 

4347 lon0 = (par['xmin'] + par['xmax'])/2. 

4348 lat0 = (par['ymin'] + par['ymax'])/2. 

4349 sll = '%g/%g' % (lon0, lat0) 

4350 widget['J'] = '-JM' + sll + '/%(width)gp' 

4351 scaler['B'] = \ 

4352 '-B%(xinc)gg%(xinc)g:%(xlabel)s:/%(yinc)gg%(yinc)g:%(ylabel)s:WSen' 

4353 

4354 

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

4356 innerticks=True): 

4357 

4358 par = scaleguru.get_params() 

4359 par_ax = scaleguru.get_params(ax_projection=True) 

4360 nz_palette = int(widget.height()/inch * 300) 

4361 px = num.zeros(nz_palette*2) 

4362 px[1::2] += 1 

4363 pz = num.linspace(par['zmin'], par['zmax'], nz_palette).repeat(2) 

4364 pdz = pz[2]-pz[0] 

4365 palgrdfile = gmt.tempfilename() 

4366 pal_r = (0, 1, par['zmin'], par['zmax']) 

4367 pal_ax_r = (0, 1, par_ax['zmin'], par_ax['zmax']) 

4368 gmt.xyz2grd( 

4369 G=palgrdfile, R=pal_r, 

4370 I=(1, pdz), in_columns=(px, pz, pz), # noqa 

4371 out_discard=True) 

4372 

4373 gmt.grdimage(palgrdfile, R=pal_r, C=cptfile, *widget.JXY()) 

4374 if isinstance(innerticks, str): 

4375 tickpen = innerticks 

4376 gmt.grdcontour(palgrdfile, W=tickpen, R=pal_r, C=cptfile, 

4377 *widget.JXY()) 

4378 

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

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

4381 ticklen = negpalwid 

4382 else: 

4383 ticklen = '0p' 

4384 

4385 TICK_LENGTH_PARAM = 'MAP_TICK_LENGTH' if gmt.is_gmt5() else 'TICK_LENGTH' 

4386 gmt.psbasemap( 

4387 R=pal_ax_r, B='4::/%(zinc)g::nsw' % par_ax, 

4388 config={TICK_LENGTH_PARAM: ticklen}, 

4389 *widget.JXY()) 

4390 

4391 if innerticks: 

4392 gmt.psbasemap( 

4393 R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax, 

4394 config={TICK_LENGTH_PARAM: '0p'}, 

4395 *widget.JXY()) 

4396 else: 

4397 gmt.psbasemap(R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax, *widget.JXY()) 

4398 

4399 if par_ax['zlabel']: 

4400 label_font = gmt.label_font() 

4401 label_font_size = gmt.label_font_size() 

4402 label_offset = zlabeloffset 

4403 gmt.pstext( 

4404 R=(0, 1, 0, 2), D="%gp/0p" % label_offset, 

4405 N=True, 

4406 in_rows=[(1, 1, label_font_size, -90, label_font, 'CB', 

4407 par_ax['zlabel'])], 

4408 *widget.JXY())