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['4.3.1'] = {'home': '/sw/share/gmt', 

327# 'bin': '/sw/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 if major_version == '6': 

1302 raise GMTInstallationProblem( 

1303 'GMT version 6 not supported by pyrocko.gmtpy.') 

1304 

1305 if major_version != '5': 

1306 gmtdefaults = pjoin(bin_dir, 'gmtdefaults') 

1307 

1308 versionfound = get_gmt_version(gmtdefaults, home_dir) 

1309 

1310 if versionfound != version: 

1311 raise GMTInstallationProblem(( 

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

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

1314 version, versionfound, gmtdefaults)) 

1315 

1316 

1317def get_gmt_installation(version): 

1318 setup_gmt_installations() 

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

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

1321 % (version, newest_installed_gmt_version())) 

1322 

1323 version = 'newest' 

1324 

1325 if version == 'newest': 

1326 version = newest_installed_gmt_version() 

1327 

1328 installation = dict(_gmt_installations[version]) 

1329 

1330 return installation 

1331 

1332 

1333def setup_gmt_installations(): 

1334 if not setup_gmt_installations.have_done: 

1335 if not _gmt_installations: 

1336 

1337 _gmt_installations.update(detect_gmt_installations()) 

1338 

1339 # store defaults as dicts into the gmt installations dicts 

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

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

1342 installation['version'] = version 

1343 

1344 for installation in _gmt_installations.values(): 

1345 check_gmt_installation(installation) 

1346 

1347 setup_gmt_installations.have_done = True 

1348 

1349 

1350setup_gmt_installations.have_done = False 

1351 

1352_paper_sizes_a = '''A0 2380 3368 

1353 A1 1684 2380 

1354 A2 1190 1684 

1355 A3 842 1190 

1356 A4 595 842 

1357 A5 421 595 

1358 A6 297 421 

1359 A7 210 297 

1360 A8 148 210 

1361 A9 105 148 

1362 A10 74 105 

1363 B0 2836 4008 

1364 B1 2004 2836 

1365 B2 1418 2004 

1366 B3 1002 1418 

1367 B4 709 1002 

1368 B5 501 709 

1369 archA 648 864 

1370 archB 864 1296 

1371 archC 1296 1728 

1372 archD 1728 2592 

1373 archE 2592 3456 

1374 flsa 612 936 

1375 halfletter 396 612 

1376 note 540 720 

1377 letter 612 792 

1378 legal 612 1008 

1379 11x17 792 1224 

1380 ledger 1224 792''' 

1381 

1382 

1383_paper_sizes = {} 

1384 

1385 

1386def setup_paper_sizes(): 

1387 if not _paper_sizes: 

1388 for line in _paper_sizes_a.splitlines(): 

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

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

1391 

1392 

1393def get_paper_size(k): 

1394 setup_paper_sizes() 

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

1396 

1397 

1398def all_paper_sizes(): 

1399 setup_paper_sizes() 

1400 return _paper_sizes 

1401 

1402 

1403def measure_unit(gmt_config): 

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

1405 if k in gmt_config: 

1406 return gmt_config[k] 

1407 

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

1409 

1410 

1411def paper_media(gmt_config): 

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

1413 if k in gmt_config: 

1414 return gmt_config[k] 

1415 

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

1417 

1418 

1419def page_orientation(gmt_config): 

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

1421 if k in gmt_config: 

1422 return gmt_config[k] 

1423 

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

1425 

1426 

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

1428 

1429 leftmargin, topmargin, rightmargin, bottommargin = margins 

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

1431 

1432 paper_size = get_paper_size(paper_media(gmt_config)) 

1433 if not portrait: 

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

1435 

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

1437 2.0 + leftmargin 

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

1439 2.0 + bottommargin 

1440 

1441 if portrait: 

1442 bb1 = int((xoffset - leftmargin)) 

1443 bb2 = int((yoffset - bottommargin)) 

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

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

1446 else: 

1447 bb1 = int((yoffset - topmargin)) 

1448 bb2 = int((xoffset - leftmargin)) 

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

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

1451 

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

1453 

1454 

1455def gmtdefaults_as_text(version='newest'): 

1456 

1457 ''' 

1458 Get the built-in gmtdefaults. 

1459 ''' 

1460 

1461 if version not in _gmt_installations: 

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

1463 % (version, newest_installed_gmt_version())) 

1464 version = 'newest' 

1465 

1466 if version == 'newest': 

1467 version = newest_installed_gmt_version() 

1468 

1469 return _gmt_defaults_by_version[version] 

1470 

1471 

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

1473 ''' 

1474 Write COARDS compliant netcdf (grd) file. 

1475 ''' 

1476 

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

1478 ny, nx = z.shape 

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

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

1481 

1482 if naming == 'xy': 

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

1484 else: 

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

1486 

1487 nc.node_offset = 0 

1488 if title is not None: 

1489 nc.title = title 

1490 

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

1492 nc.createDimension(kx, nx) 

1493 nc.createDimension(ky, ny) 

1494 

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

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

1497 if naming == 'xy': 

1498 xvar.long_name = kx 

1499 yvar.long_name = ky 

1500 else: 

1501 xvar.long_name = 'longitude' 

1502 xvar.units = 'degrees_east' 

1503 yvar.long_name = 'latitude' 

1504 yvar.units = 'degrees_north' 

1505 

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

1507 

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

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

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

1511 

1512 nc.close() 

1513 

1514 

1515def to_array(var): 

1516 arr = var[:].copy() 

1517 if hasattr(var, 'scale_factor'): 

1518 arr *= var.scale_factor 

1519 

1520 if hasattr(var, 'add_offset'): 

1521 arr += var.add_offset 

1522 

1523 return arr 

1524 

1525 

1526def loadgrd(filename): 

1527 ''' 

1528 Read COARDS compliant netcdf (grd) file. 

1529 ''' 

1530 

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

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

1533 kx = 'x' 

1534 ky = 'y' 

1535 if 'lon' in vkeys: 

1536 kx = 'lon' 

1537 if 'lat' in vkeys: 

1538 ky = 'lat' 

1539 

1540 kz = 'z' 

1541 if 'altitude' in vkeys: 

1542 kz = 'altitude' 

1543 

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

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

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

1547 

1548 nc.close() 

1549 return x, y, z 

1550 

1551 

1552def centers_to_edges(asorted): 

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

1554 

1555 

1556def nvals(asorted): 

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

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

1559 

1560 

1561def guess_vals(asorted): 

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

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

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

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

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

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

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

1569 

1570 

1571def blockmean(asorted, b): 

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

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

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

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

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

1577 return ( 

1578 asorted[indis[:-1]], 

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

1580 

1581 

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

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

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

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

1586 

1587 zindi = yindi*nx+xindi 

1588 order = num.argsort(zindi) 

1589 z = z[order] 

1590 zindi = zindi[order] 

1591 

1592 zindi, z = blockmean(zindi, z) 

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

1594 znew[:] = num.nan 

1595 znew[zindi] = z 

1596 return znew.reshape(ny, nx) 

1597 

1598 

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

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

1601 xs = x_sorted 

1602 ys = y_sorted 

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

1604 if mode == 'nonrandom': 

1605 return nxs, nys, 0 

1606 elif xs.size == nxs*nys: 

1607 # exact match 

1608 return nxs, nys, 0 

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

1610 # possibly randomly sampled 

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

1612 nys = nxs 

1613 return nxs, nys, 2 

1614 else: 

1615 return nxs, nys, 1 

1616 

1617 

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

1619 ''' 

1620 Grid tabular XYZ data by binning. 

1621 

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

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

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

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

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

1627 

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

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

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

1631 ''' 

1632 

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

1634 assert x.size == y.size == z.size 

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

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

1637 if badness <= 1: 

1638 xf = guess_vals(xs) 

1639 yf = guess_vals(ys) 

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

1641 else: 

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

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

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

1645 

1646 return xf, yf, zf 

1647 

1648 

1649def tabledata(xf, yf, zf): 

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

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

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

1653 z = zf.flatten() 

1654 return x, y, z 

1655 

1656 

1657def double1d(a): 

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

1659 a2[::2] = a 

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

1661 return a2 

1662 

1663 

1664def double2d(f): 

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

1666 f2[:, :] = num.nan 

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

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

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

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

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

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

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

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

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

1676 return f2 

1677 

1678 

1679def doublegrid(x, y, z): 

1680 x2 = double1d(x) 

1681 y2 = double1d(y) 

1682 z2 = double2d(z) 

1683 return x2, y2, z2 

1684 

1685 

1686class Guru(object): 

1687 ''' 

1688 Abstract base class providing template interpolation, accessible as 

1689 attributes. 

1690 

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

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

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

1694 with the templates. 

1695 ''' 

1696 

1697 def __init__(self): 

1698 self.templates = {} 

1699 

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

1701 params = self.get_params(**kwargs) 

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

1703 return strings 

1704 

1705 # hand through templates dict 

1706 def __getitem__(self, template_name): 

1707 return self.templates[template_name] 

1708 

1709 def __setitem__(self, template_name, template): 

1710 self.templates[template_name] = template 

1711 

1712 def __contains__(self, template_name): 

1713 return template_name in self.templates 

1714 

1715 def __iter__(self): 

1716 return iter(self.templates) 

1717 

1718 def __len__(self): 

1719 return len(self.templates) 

1720 

1721 def __delitem__(self, template_name): 

1722 del(self.templates[template_name]) 

1723 

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

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

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

1727 

1728 def __getattr__(self, template_names): 

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

1730 raise AttributeError(template_names) 

1731 

1732 def f(**kwargs): 

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

1734 

1735 return f 

1736 

1737 

1738def nice_value(x): 

1739 ''' 

1740 Round ``x`` to nice value. 

1741 ''' 

1742 

1743 exp = 1.0 

1744 sign = 1 

1745 if x < 0.0: 

1746 x = -x 

1747 sign = -1 

1748 while x >= 1.0: 

1749 x /= 10.0 

1750 exp *= 10.0 

1751 while x < 0.1: 

1752 x *= 10.0 

1753 exp /= 10.0 

1754 

1755 if x >= 0.75: 

1756 return sign * 1.0 * exp 

1757 if x >= 0.375: 

1758 return sign * 0.5 * exp 

1759 if x >= 0.225: 

1760 return sign * 0.25 * exp 

1761 if x >= 0.15: 

1762 return sign * 0.2 * exp 

1763 

1764 return sign * 0.1 * exp 

1765 

1766 

1767class AutoScaler(object): 

1768 ''' 

1769 Tunable 1D autoscaling based on data range. 

1770 

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

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

1773 notation. 

1774 

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

1776 

1777 .. py:attribute:: approx_ticks 

1778 

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

1780 

1781 .. py:attribute:: mode 

1782 

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

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

1785 

1786 ================ ================================================== 

1787 mode description 

1788 ================ ================================================== 

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

1790 below. 

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

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

1793 max. 

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

1795 zero. 

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

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

1798 disabled, such that the output range always 

1799 exactly matches the data range. 

1800 ================ ================================================== 

1801 

1802 .. py:attribute:: exp 

1803 

1804 If defined, override automatically determined exponent for notation 

1805 by the given value. 

1806 

1807 .. py:attribute:: snap 

1808 

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

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

1811 

1812 .. py:attribute:: inc 

1813 

1814 If defined, override automatically determined tick increment by the 

1815 given value. 

1816 

1817 .. py:attribute:: space 

1818 

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

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

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

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

1823 

1824 .. py:attribute:: exp_factor 

1825 

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

1827 

1828 .. py:attribute:: no_exp_interval: 

1829 

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

1831 

1832 ''' 

1833 

1834 def __init__( 

1835 self, 

1836 approx_ticks=7.0, 

1837 mode='auto', 

1838 exp=None, 

1839 snap=False, 

1840 inc=None, 

1841 space=0.0, 

1842 exp_factor=3, 

1843 no_exp_interval=(-3, 5)): 

1844 

1845 ''' 

1846 Create new AutoScaler instance. 

1847 

1848 The parameters are described in the AutoScaler documentation. 

1849 ''' 

1850 

1851 self.approx_ticks = approx_ticks 

1852 self.mode = mode 

1853 self.exp = exp 

1854 self.snap = snap 

1855 self.inc = inc 

1856 self.space = space 

1857 self.exp_factor = exp_factor 

1858 self.no_exp_interval = no_exp_interval 

1859 

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

1861 

1862 ''' 

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

1864 

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

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

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

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

1869 value. 

1870 ''' 

1871 

1872 data_min = min(data_range) 

1873 data_max = max(data_range) 

1874 

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

1876 

1877 a = self.mode 

1878 if self.mode == 'auto': 

1879 a = self.guess_autoscale_mode(data_min, data_max) 

1880 

1881 if override_mode is not None: 

1882 a = override_mode 

1883 

1884 mi, ma = 0, 0 

1885 if a == 'off': 

1886 mi, ma = data_min, data_max 

1887 elif a == '0-max': 

1888 mi = 0.0 

1889 if data_max > 0.0: 

1890 ma = data_max 

1891 else: 

1892 ma = 1.0 

1893 elif a == 'min-0': 

1894 ma = 0.0 

1895 if data_min < 0.0: 

1896 mi = data_min 

1897 else: 

1898 mi = -1.0 

1899 elif a == 'min-max': 

1900 mi, ma = data_min, data_max 

1901 elif a == 'symmetric': 

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

1903 mi = -m 

1904 ma = m 

1905 

1906 nmi = mi 

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

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

1909 

1910 nma = ma 

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

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

1913 

1914 mi, ma = nmi, nma 

1915 

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

1917 mi -= 1.0 

1918 ma += 1.0 

1919 

1920 # make nice tick increment 

1921 if self.inc is not None: 

1922 inc = self.inc 

1923 else: 

1924 if self.approx_ticks > 0.: 

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

1926 else: 

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

1928 

1929 if inc == 0.0: 

1930 inc = 1.0 

1931 

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

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

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

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

1936 

1937 if is_reverse: 

1938 return ma, mi, -inc 

1939 else: 

1940 return mi, ma, inc 

1941 

1942 def make_exp(self, x): 

1943 ''' 

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

1945 

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

1947 ''' 

1948 

1949 if self.exp is not None: 

1950 return self.exp 

1951 

1952 x = abs(x) 

1953 if x == 0.0: 

1954 return 0 

1955 

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

1957 return 0 

1958 

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

1960 

1961 def guess_autoscale_mode(self, data_min, data_max): 

1962 ''' 

1963 Guess mode of operation, based on data range. 

1964 

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

1966 or ``'symmetric'``. 

1967 ''' 

1968 

1969 a = 'min-max' 

1970 if data_min >= 0.0: 

1971 if data_min < data_max/2.: 

1972 a = '0-max' 

1973 else: 

1974 a = 'min-max' 

1975 if data_max <= 0.0: 

1976 if data_max > data_min/2.: 

1977 a = 'min-0' 

1978 else: 

1979 a = 'min-max' 

1980 if data_min < 0.0 and data_max > 0.0: 

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

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

1983 a = 'symmetric' 

1984 else: 

1985 a = 'min-max' 

1986 return a 

1987 

1988 

1989class Ax(AutoScaler): 

1990 ''' 

1991 Ax description with autoscaling capabilities. 

1992 

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

1994 the following additional attributes (with default values given in 

1995 paranthesis): 

1996 

1997 .. py:attribute:: label 

1998 

1999 Ax label (without unit). 

2000 

2001 .. py:attribute:: unit 

2002 

2003 Physical unit of the data attached to this ax. 

2004 

2005 .. py:attribute:: scaled_unit 

2006 

2007 (see below) 

2008 

2009 .. py:attribute:: scaled_unit_factor 

2010 

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

2012 

2013 unit = scaled_unit_factor x scaled_unit. 

2014 

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

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

2017 1e9.) 

2018 

2019 .. py:attribute:: limits 

2020 

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

2022 

2023 .. py:attribute:: masking 

2024 

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

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

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

2028 

2029 ''' 

2030 

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

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

2033 

2034 AutoScaler.__init__(self, **kwargs) 

2035 self.label = label 

2036 self.unit = unit 

2037 self.scaled_unit_factor = scaled_unit_factor 

2038 self.scaled_unit = scaled_unit 

2039 self.limits = limits 

2040 self.masking = masking 

2041 

2042 def label_str(self, exp, unit): 

2043 ''' 

2044 Get label string including the unit and multiplier. 

2045 ''' 

2046 

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

2048 if self.label: 

2049 slabel = self.label 

2050 

2051 if unit or exp != 0: 

2052 if exp != 0: 

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

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

2055 else: 

2056 sunit = '[ %s ]' % unit 

2057 

2058 p = [] 

2059 if slabel: 

2060 p.append(slabel) 

2061 

2062 if sunit: 

2063 p.append(sunit) 

2064 

2065 return ' '.join(p) 

2066 

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

2068 override_scaled_unit_factor=None): 

2069 

2070 ''' 

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

2072 

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

2074 multiplier for given data range. 

2075 

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

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

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

2079 scaling applied. 

2080 ''' 

2081 

2082 sf = self.scaled_unit_factor 

2083 

2084 if override_scaled_unit_factor is not None: 

2085 sf = override_scaled_unit_factor 

2086 

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

2088 

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

2090 if self.inc is not None: 

2091 inc = self.inc*sf 

2092 

2093 if ax_projection: 

2094 exp = self.make_exp(inc) 

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

2096 unit = self.unit 

2097 else: 

2098 unit = self.scaled_unit 

2099 label = self.label_str(exp, unit) 

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

2101 else: 

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

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

2104 

2105 

2106class ScaleGuru(Guru): 

2107 

2108 ''' 

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

2110 

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

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

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

2114 arguments, which are required for most GMT commands. 

2115 

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

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

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

2119 

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

2121 

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

2123 limits imposed on other axes. 

2124 

2125 ''' 

2126 

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

2128 percent_interval=None, copy_from=None): 

2129 

2130 Guru.__init__(self) 

2131 

2132 if copy_from: 

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

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

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

2136 self.aspect = copy_from.aspect 

2137 

2138 if percent_interval is not None: 

2139 from scipy.stats import scoreatpercentile as scap 

2140 

2141 self.templates = dict( 

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

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

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

2145 

2146 maxdim = 2 

2147 if data_tuples: 

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

2149 else: 

2150 if axes: 

2151 maxdim = len(axes) 

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

2153 if axes is not None: 

2154 self.axes = axes 

2155 else: 

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

2157 

2158 # sophisticated data-range calculation 

2159 data_ranges = [None] * maxdim 

2160 for dt_ in data_tuples: 

2161 dt = num.asarray(dt_) 

2162 in_range = True 

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

2164 if ax.limits and ax.masking: 

2165 ax_limits = list(ax.limits) 

2166 if ax_limits[0] is None: 

2167 ax_limits[0] = -num.inf 

2168 if ax_limits[1] is None: 

2169 ax_limits[1] = num.inf 

2170 in_range = num.logical_and( 

2171 in_range, 

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

2173 

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

2175 

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

2177 if len(x) >= 1: 

2178 if in_range is not True: 

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

2180 if percent_interval is None: 

2181 range_this = ( 

2182 num.nanmin(xmasked), 

2183 num.nanmax(xmasked)) 

2184 else: 

2185 xmasked_finite = num.compress( 

2186 num.isfinite(xmasked), xmasked) 

2187 range_this = ( 

2188 scap(xmasked_finite, 

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

2190 scap(xmasked_finite, 

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

2192 else: 

2193 if percent_interval is None: 

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

2195 else: 

2196 xmasked_finite = num.compress( 

2197 num.isfinite(xmasked), xmasked) 

2198 range_this = ( 

2199 scap(xmasked_finite, 

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

2201 scap(xmasked_finite, 

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

2203 else: 

2204 range_this = (0., 1.) 

2205 

2206 if ax.limits: 

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

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

2209 range_this[1]) 

2210 

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

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

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

2214 

2215 else: 

2216 range_this = ax.limits 

2217 

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

2219 data_ranges[i] = range_this 

2220 else: 

2221 mi, ma = range_this 

2222 if data_ranges[i] is not None: 

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

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

2225 

2226 data_ranges[i] = (mi, ma) 

2227 

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

2229 if data_ranges[i] is None or not ( 

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

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

2232 

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

2234 

2235 self.data_ranges = data_ranges 

2236 self.aspect = aspect 

2237 

2238 def copy(self): 

2239 return ScaleGuru(copy_from=self) 

2240 

2241 def get_params(self, ax_projection=False): 

2242 

2243 ''' 

2244 Get dict with output parameters. 

2245 

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

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

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

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

2250 

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

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

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

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

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

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

2257 label string. 

2258 ''' 

2259 

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

2261 self.data_ranges[0], ax_projection) 

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

2263 self.data_ranges[1], ax_projection) 

2264 if len(self.axes) > 2: 

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

2266 self.data_ranges[2], ax_projection) 

2267 

2268 # enforce certain aspect, if needed 

2269 if self.aspect is not None: 

2270 xwid = xma-xmi 

2271 ywid = yma-ymi 

2272 if ywid < xwid*self.aspect: 

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

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

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

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

2277 override_scaled_unit_factor=1.) 

2278 

2279 elif xwid < ywid/self.aspect: 

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

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

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

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

2284 override_scaled_unit_factor=1.) 

2285 

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

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

2288 if len(self.axes) > 2: 

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

2290 

2291 return params 

2292 

2293 

2294class GumSpring(object): 

2295 

2296 ''' 

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

2298 ''' 

2299 

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

2301 self.minimal = minimal 

2302 if grow is None: 

2303 if minimal is None: 

2304 self.grow = 1.0 

2305 else: 

2306 self.grow = 0.0 

2307 else: 

2308 self.grow = grow 

2309 self.value = 1.0 

2310 

2311 def get_minimal(self): 

2312 if self.minimal is not None: 

2313 return self.minimal 

2314 else: 

2315 return 0.0 

2316 

2317 def get_grow(self): 

2318 return self.grow 

2319 

2320 def set_value(self, value): 

2321 self.value = value 

2322 

2323 def get_value(self): 

2324 return self.value 

2325 

2326 

2327def distribute(sizes, grows, space): 

2328 sizes = list(sizes) 

2329 gsum = sum(grows) 

2330 if gsum > 0.0: 

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

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

2333 return sizes 

2334 

2335 

2336class Widget(Guru): 

2337 

2338 ''' 

2339 Base class of the gmtpy layout system. 

2340 

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

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

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

2344 

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

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

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

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

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

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

2351 

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

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

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

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

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

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

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

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

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

2361 

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

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

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

2365 ''' 

2366 

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

2368 

2369 ''' 

2370 Create new widget. 

2371 ''' 

2372 

2373 Guru.__init__(self) 

2374 

2375 self.templates = dict( 

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

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

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

2379 

2380 if horizontal is None: 

2381 self.horizontal = GumSpring() 

2382 else: 

2383 self.horizontal = horizontal 

2384 

2385 if vertical is None: 

2386 self.vertical = GumSpring() 

2387 else: 

2388 self.vertical = vertical 

2389 

2390 self.aspect = None 

2391 self.parent = parent 

2392 self.dirty = True 

2393 

2394 def set_parent(self, parent): 

2395 

2396 ''' 

2397 Set the parent widget. 

2398 

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

2400 methods are responsible for calling this. 

2401 ''' 

2402 

2403 self.parent = parent 

2404 self.dirtyfy() 

2405 

2406 def get_parent(self): 

2407 

2408 ''' 

2409 Get the widgets parent widget. 

2410 ''' 

2411 

2412 return self.parent 

2413 

2414 def get_root(self): 

2415 

2416 ''' 

2417 Get the root widget in the layout hierarchy. 

2418 ''' 

2419 

2420 if self.parent is not None: 

2421 return self.get_parent() 

2422 else: 

2423 return self 

2424 

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

2426 

2427 ''' 

2428 Set the horizontal sizing policy of the Widget. 

2429 

2430 

2431 :param minimal: new minimal width of the widget 

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

2433 ''' 

2434 

2435 self.horizontal = GumSpring(minimal, grow) 

2436 self.dirtyfy() 

2437 

2438 def get_horizontal(self): 

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

2440 

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

2442 

2443 ''' 

2444 Set the horizontal sizing policy of the Widget. 

2445 

2446 :param minimal: new minimal height of the widget 

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

2448 ''' 

2449 

2450 self.vertical = GumSpring(minimal, grow) 

2451 self.dirtyfy() 

2452 

2453 def get_vertical(self): 

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

2455 

2456 def set_aspect(self, aspect=None): 

2457 

2458 ''' 

2459 Set aspect constraint on the widget. 

2460 

2461 The aspect is given as height divided by width. 

2462 ''' 

2463 

2464 self.aspect = aspect 

2465 self.dirtyfy() 

2466 

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

2468 

2469 ''' 

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

2471 call. 

2472 ''' 

2473 

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

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

2476 self.set_aspect(aspect) 

2477 

2478 def get_policy(self): 

2479 mh, gh = self.get_horizontal() 

2480 mv, gv = self.get_vertical() 

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

2482 

2483 def legalize(self, size, offset): 

2484 

2485 ''' 

2486 Get legal size for widget. 

2487 

2488 Returns: (new_size, new_offset) 

2489 

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

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

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

2493 ''' 

2494 

2495 sh, sv = size 

2496 oh, ov = offset 

2497 shs, svs = Widget.get_min_size(self) 

2498 ghs, gvs = Widget.get_grow(self) 

2499 

2500 if ghs == 0.0: 

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

2502 sh = shs 

2503 

2504 if gvs == 0.0: 

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

2506 sv = svs 

2507 

2508 if self.aspect is not None: 

2509 if sh > sv/self.aspect: 

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

2511 sh = sv/self.aspect 

2512 if sv > sh*self.aspect: 

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

2514 sv = sh*self.aspect 

2515 

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

2517 

2518 def get_min_size(self): 

2519 

2520 ''' 

2521 Get minimum size of widget. 

2522 

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

2524 ''' 

2525 

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

2527 if self.aspect is not None: 

2528 if mv == 0.0: 

2529 return mh, mh*self.aspect 

2530 elif mh == 0.0: 

2531 return mv/self.aspect, mv 

2532 return mh, mv 

2533 

2534 def get_grow(self): 

2535 

2536 ''' 

2537 Get widget's desire to grow. 

2538 

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

2540 ''' 

2541 

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

2543 

2544 def set_size(self, size, offset): 

2545 

2546 ''' 

2547 Set the widget's current size. 

2548 

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

2550 responsibility to call this. 

2551 ''' 

2552 

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

2554 self.offset = inner_offset 

2555 self.horizontal.set_value(sh) 

2556 self.vertical.set_value(sv) 

2557 self.dirty = False 

2558 

2559 def __str__(self): 

2560 

2561 def indent(ind, str): 

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

2563 size, offset = self.get_size() 

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

2565 children = self.get_children() 

2566 if children: 

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

2568 return s 

2569 

2570 def policies_debug_str(self): 

2571 

2572 def indent(ind, str): 

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

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

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

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

2577 

2578 children = self.get_children() 

2579 if children: 

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

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

2582 return s 

2583 

2584 def get_corners(self, descend=False): 

2585 

2586 ''' 

2587 Get coordinates of the corners of the widget. 

2588 

2589 Returns list with coordinate tuples. 

2590 

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

2592 coordinates of all sub-widgets. 

2593 ''' 

2594 

2595 self.do_layout() 

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

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

2598 if descend: 

2599 for child in self.get_children(): 

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

2601 return corners 

2602 

2603 def get_sizes(self): 

2604 

2605 ''' 

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

2607 

2608 Returns a list with size tuples. 

2609 ''' 

2610 self.do_layout() 

2611 sizes = [self.get_size()] 

2612 for child in self.get_children(): 

2613 sizes.extend(child.get_sizes()) 

2614 return sizes 

2615 

2616 def do_layout(self): 

2617 

2618 ''' 

2619 Triggers layouting of the widget hierarchy, if needed. 

2620 ''' 

2621 

2622 if self.parent is not None: 

2623 return self.parent.do_layout() 

2624 

2625 if not self.dirty: 

2626 return 

2627 

2628 sh, sv = self.get_min_size() 

2629 gh, gv = self.get_grow() 

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

2631 sh = 15.*cm 

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

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

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

2635 

2636 def get_children(self): 

2637 

2638 ''' 

2639 Get sub-widgets contained in this widget. 

2640 

2641 Returns a list of widgets. 

2642 ''' 

2643 

2644 return [] 

2645 

2646 def get_size(self): 

2647 

2648 ''' 

2649 Get current size and position of the widget. 

2650 

2651 Triggers layouting and returns 

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

2653 ''' 

2654 

2655 self.do_layout() 

2656 return (self.horizontal.get_value(), 

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

2658 

2659 def get_params(self): 

2660 

2661 ''' 

2662 Get current size and position of the widget. 

2663 

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

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

2666 ''' 

2667 

2668 self.do_layout() 

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

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

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

2672 

2673 def width(self): 

2674 

2675 ''' 

2676 Get current width of the widget. 

2677 

2678 Triggers layouting and returns width. 

2679 ''' 

2680 

2681 self.do_layout() 

2682 return self.horizontal.get_value() 

2683 

2684 def height(self): 

2685 

2686 ''' 

2687 Get current height of the widget. 

2688 

2689 Triggers layouting and return height. 

2690 ''' 

2691 

2692 self.do_layout() 

2693 return self.vertical.get_value() 

2694 

2695 def bbox(self): 

2696 

2697 ''' 

2698 Get PostScript bounding box for this widget. 

2699 

2700 Triggers layouting and returns values suitable to create PS bounding 

2701 box, representing the widgets current size and position. 

2702 ''' 

2703 

2704 self.do_layout() 

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

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

2707 

2708 def dirtyfy(self): 

2709 

2710 ''' 

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

2712 

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

2714 new layouting. 

2715 ''' 

2716 

2717 if self.parent is not None: 

2718 self.parent.dirtyfy() 

2719 

2720 self.dirty = True 

2721 

2722 

2723class CenterLayout(Widget): 

2724 

2725 ''' 

2726 A layout manager which centers its single child widget. 

2727 

2728 The child widget may be oversized. 

2729 ''' 

2730 

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

2732 Widget.__init__(self, horizontal, vertical) 

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

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

2735 

2736 def get_min_size(self): 

2737 shs, svs = Widget.get_min_size(self) 

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

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

2740 

2741 def get_grow(self): 

2742 ghs, gvs = Widget.get_grow(self) 

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

2744 return gh*ghs, gv*gvs 

2745 

2746 def set_size(self, size, offset): 

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

2748 

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

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

2751 if ghc != 0.: 

2752 shc = sh 

2753 if gvc != 0.: 

2754 svc = sv 

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

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

2757 

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

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

2760 

2761 def set_widget(self, widget=None): 

2762 

2763 ''' 

2764 Set the child widget, which shall be centered. 

2765 ''' 

2766 

2767 if widget is None: 

2768 widget = Widget() 

2769 

2770 self.content = widget 

2771 

2772 widget.set_parent(self) 

2773 

2774 def get_widget(self): 

2775 return self.content 

2776 

2777 def get_children(self): 

2778 return [self.content] 

2779 

2780 

2781class FrameLayout(Widget): 

2782 

2783 ''' 

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

2785 widgets. 

2786 

2787 :: 

2788 

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

2790 | top | 

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

2792 | | | | 

2793 | left | center | right | 

2794 | | | | 

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

2796 | bottom | 

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

2798 

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

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

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

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

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

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

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

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

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

2808 spaces between the widgets. 

2809 ''' 

2810 

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

2812 Widget.__init__(self, horizontal, vertical) 

2813 mw = 3.*cm 

2814 self.left = Widget( 

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

2816 self.right = Widget( 

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

2818 self.top = Widget( 

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

2820 parent=self) 

2821 self.bottom = Widget( 

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

2823 parent=self) 

2824 self.center = Widget( 

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

2826 parent=self) 

2827 

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

2829 ''' 

2830 Give margins fixed size constraints. 

2831 ''' 

2832 

2833 self.left.set_horizontal(left, 0) 

2834 self.right.set_horizontal(right, 0) 

2835 self.top.set_vertical(top, 0) 

2836 self.bottom.set_vertical(bottom, 0) 

2837 

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

2839 ''' 

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

2841 

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

2843 ''' 

2844 self.left.set_horizontal(left, grow) 

2845 self.right.set_horizontal(right, grow) 

2846 self.top.set_vertical(top, grow) 

2847 self.bottom.set_vertical(bottom, grow) 

2848 

2849 def get_min_size(self): 

2850 shs, svs = Widget.get_min_size(self) 

2851 

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

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

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

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

2856 

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

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

2859 

2860 # prevent widgets from collapsing 

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

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

2863 shsum += 0.1*cm 

2864 

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

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

2867 svsum += 0.1*cm 

2868 

2869 sh = max(shs, shsum) 

2870 sv = max(svs, svsum) 

2871 

2872 return sh, sv 

2873 

2874 def get_grow(self): 

2875 ghs, gvs = Widget.get_grow(self) 

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

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

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

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

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

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

2882 return gh, gv 

2883 

2884 def set_size(self, size, offset): 

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

2886 

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

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

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

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

2891 

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

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

2894 

2895 if ah < 0.0: 

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

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

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

2899 if av < 0.0: 

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

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

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

2903 

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

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

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

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

2908 

2909 if self.center.aspect is not None: 

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

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

2912 if 0.0 < ahm < ah: 

2913 slh, srh, sch = distribute( 

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

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

2916 

2917 elif 0.0 < avm < av: 

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

2919 sch*self.center.aspect), 

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

2921 

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

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

2924 

2925 oh += ah/2. 

2926 ov += av/2. 

2927 sh -= ah 

2928 sv -= av 

2929 

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

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

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

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

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

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

2936 

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

2938 

2939 ''' 

2940 Set one of the sub-widgets. 

2941 

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

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

2944 ''' 

2945 

2946 if widget is None: 

2947 widget = Widget() 

2948 

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

2950 self.__dict__[which] = widget 

2951 else: 

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

2953 

2954 widget.set_parent(self) 

2955 

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

2957 

2958 ''' 

2959 Get one of the sub-widgets. 

2960 

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

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

2963 ''' 

2964 

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

2966 return self.__dict__[which] 

2967 else: 

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

2969 

2970 def get_children(self): 

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

2972 

2973 

2974class GridLayout(Widget): 

2975 

2976 ''' 

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

2978 

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

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

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

2982 

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

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

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

2986 might not be resolved optimally. 

2987 ''' 

2988 

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

2990 

2991 ''' 

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

2993 ''' 

2994 

2995 Widget.__init__(self, horizontal, vertical) 

2996 self.grid = [] 

2997 for iy in range(ny): 

2998 row = [] 

2999 for ix in range(nx): 

3000 w = Widget(parent=self) 

3001 row.append(w) 

3002 

3003 self.grid.append(row) 

3004 

3005 def sub_min_sizes_as_array(self): 

3006 esh = num.array( 

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

3008 dtype=float) 

3009 esv = num.array( 

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

3011 dtype=float) 

3012 return esh, esv 

3013 

3014 def sub_grows_as_array(self): 

3015 egh = num.array( 

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

3017 dtype=float) 

3018 egv = num.array( 

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

3020 dtype=float) 

3021 return egh, egv 

3022 

3023 def get_min_size(self): 

3024 sh, sv = Widget.get_min_size(self) 

3025 esh, esv = self.sub_min_sizes_as_array() 

3026 if esh.size != 0: 

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

3028 if esv.size != 0: 

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

3030 return sh, sv 

3031 

3032 def get_grow(self): 

3033 ghs, gvs = Widget.get_grow(self) 

3034 egh, egv = self.sub_grows_as_array() 

3035 if egh.size != 0: 

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

3037 else: 

3038 gh = 1.0 

3039 if egv.size != 0: 

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

3041 else: 

3042 gv = 1.0 

3043 return gh, gv 

3044 

3045 def set_size(self, size, offset): 

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

3047 esh, esv = self.sub_min_sizes_as_array() 

3048 egh, egv = self.sub_grows_as_array() 

3049 

3050 # available additional space 

3051 empty = esh.size == 0 

3052 

3053 if not empty: 

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

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

3056 else: 

3057 av = sv 

3058 ah = sh 

3059 

3060 if ah < 0.0: 

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

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

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

3064 if av < 0.0: 

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

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

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

3068 

3069 nx, ny = esh.shape 

3070 

3071 if not empty: 

3072 # distribute additional space on rows and columns 

3073 # according to grow weights and minimal sizes 

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

3075 nesh = esh.copy() 

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

3077 

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

3079 

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

3081 nesv = esv.copy() 

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

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

3084 

3085 ah = sh - sum(nsh) 

3086 av = sv - sum(nsv) 

3087 

3088 oh += ah/2. 

3089 ov += av/2. 

3090 sh -= ah 

3091 sv -= av 

3092 

3093 # resize child widgets 

3094 neov = ov + sum(nsv) 

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

3096 neov -= nesv 

3097 neoh = oh 

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

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

3100 neoh += nesh 

3101 

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

3103 

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

3105 

3106 ''' 

3107 Set one of the sub-widgets. 

3108 

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

3110 counted from zero. 

3111 ''' 

3112 

3113 if widget is None: 

3114 widget = Widget() 

3115 

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

3117 widget.set_parent(self) 

3118 

3119 def get_widget(self, ix, iy): 

3120 

3121 ''' 

3122 Get one of the sub-widgets. 

3123 

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

3125 counted from zero. 

3126 ''' 

3127 

3128 return self.grid[iy][ix] 

3129 

3130 def get_children(self): 

3131 children = [] 

3132 for row in self.grid: 

3133 children.extend(row) 

3134 

3135 return children 

3136 

3137 

3138def is_gmt5(version='newest'): 

3139 return get_gmt_installation(version)['version'][0] == '5' 

3140 

3141 

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

3143 

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

3145 

3146 if gmt.is_gmt5(): 

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

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

3149 gmt.save(fn, crop_eps_mode=True) 

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

3151 s = f.read() 

3152 

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

3154 else: 

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

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

3157 

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

3159 

3160 

3161def text_box( 

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

3163 

3164 gmt = GMT(version=gmtversion) 

3165 if gmt.is_gmt5(): 

3166 row = [0, 0, text] 

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

3168 else: 

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

3170 farg = [] 

3171 

3172 gmt.pstext( 

3173 in_rows=[row], 

3174 finish=True, 

3175 R=(0, 1, 0, 1), 

3176 J='x10p', 

3177 N=True, 

3178 *farg, 

3179 **kwargs) 

3180 

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

3182 gmt.save(fn) 

3183 

3184 (_, stderr) = subprocess.Popen( 

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

3186 stderr=subprocess.PIPE).communicate() 

3187 

3188 dx, dy = None, None 

3189 for line in stderr.splitlines(): 

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

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

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

3193 break 

3194 

3195 return dx, dy 

3196 

3197 

3198class TableLiner(object): 

3199 ''' 

3200 Utility class to turn tables into lines. 

3201 ''' 

3202 

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

3204 self.in_columns = in_columns 

3205 self.in_rows = in_rows 

3206 self.encoding = encoding 

3207 

3208 def __iter__(self): 

3209 if self.in_columns is not None: 

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

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

3212 self.encoding) 

3213 

3214 if self.in_rows is not None: 

3215 for row in self.in_rows: 

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

3217 self.encoding) 

3218 

3219 

3220class LineStreamChopper(object): 

3221 ''' 

3222 File-like object to buffer data. 

3223 ''' 

3224 

3225 def __init__(self, liner): 

3226 self.chopsize = None 

3227 self.liner = liner 

3228 self.chop_iterator = None 

3229 self.closed = False 

3230 

3231 def _chopiter(self): 

3232 buf = BytesIO() 

3233 for line in self.liner: 

3234 buf.write(line) 

3235 buflen = buf.tell() 

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

3237 buf.seek(0) 

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

3239 yield buf.read(self.chopsize) 

3240 

3241 newbuf = BytesIO() 

3242 newbuf.write(buf.read()) 

3243 buf.close() 

3244 buf = newbuf 

3245 

3246 yield(buf.getvalue()) 

3247 buf.close() 

3248 

3249 def read(self, size=None): 

3250 if self.closed: 

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

3252 if self.chop_iterator is None: 

3253 self.chopsize = size 

3254 self.chop_iterator = self._chopiter() 

3255 

3256 self.chopsize = size 

3257 try: 

3258 return next(self.chop_iterator) 

3259 except StopIteration: 

3260 return '' 

3261 

3262 def close(self): 

3263 self.chopsize = None 

3264 self.chop_iterator = None 

3265 self.closed = True 

3266 

3267 def flush(self): 

3268 pass 

3269 

3270 

3271font_tab = { 

3272 0: 'Helvetica', 

3273 1: 'Helvetica-Bold', 

3274} 

3275 

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

3277 

3278 

3279class GMT(object): 

3280 ''' 

3281 A thin wrapper to GMT command execution. 

3282 

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

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

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

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

3287 gmtpy and gmtpy must know where to find it. 

3288 

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

3290 output file. 

3291 

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

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

3294 

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

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

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

3298 

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

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

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

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

3303 

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

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

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

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

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

3309 execution of more than one GMT instance. 

3310 

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

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

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

3314 backward compatibility of the scripts can be maintained. 

3315 

3316 ''' 

3317 

3318 def __init__( 

3319 self, 

3320 config=None, 

3321 kontinue=None, 

3322 version='newest', 

3323 config_papersize=None, 

3324 eps_mode=False): 

3325 

3326 self.installation = get_gmt_installation(version) 

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

3328 self.eps_mode = eps_mode 

3329 self._shutil = shutil 

3330 

3331 if config: 

3332 self.gmt_config.update(config) 

3333 

3334 if config_papersize: 

3335 if not isinstance(config_papersize, str): 

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

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

3338 

3339 if self.is_gmt5(): 

3340 self.gmt_config['PS_MEDIA'] = config_papersize 

3341 else: 

3342 self.gmt_config['PAPER_MEDIA'] = config_papersize 

3343 

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

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

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

3347 

3348 if kontinue is not None: 

3349 self.load_unfinished(kontinue) 

3350 self.needstart = False 

3351 else: 

3352 self.output = BytesIO() 

3353 self.needstart = True 

3354 

3355 self.finished = False 

3356 

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

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

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

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

3361 

3362 self.layout = None 

3363 self.command_log = [] 

3364 self.keep_temp_dir = False 

3365 

3366 def is_gmt5(self): 

3367 return self.installation['version'][0] == '5' 

3368 

3369 def get_version(self): 

3370 return self.installation['version'] 

3371 

3372 def get_config(self, key): 

3373 return self.gmt_config[key] 

3374 

3375 def to_points(self, string): 

3376 if not string: 

3377 return 0 

3378 

3379 unit = string[-1] 

3380 if unit in _units: 

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

3382 else: 

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

3384 return float(string)/_units[default_unit] 

3385 

3386 def label_font_size(self): 

3387 if self.is_gmt5(): 

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

3389 else: 

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

3391 

3392 def label_font(self): 

3393 if self.is_gmt5(): 

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

3395 else: 

3396 return self.gmt_config['LABEL_FONT'] 

3397 

3398 def gen_gmt_config_file(self, config_filename, config): 

3399 f = open(config_filename, 'wb') 

3400 f.write( 

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

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

3403 

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

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

3406 f.close() 

3407 

3408 def __del__(self): 

3409 if not self.keep_temp_dir: 

3410 self._shutil.rmtree(self.tempdir) 

3411 

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

3413 

3414 ''' 

3415 Execute arbitrary GMT command. 

3416 

3417 See docstring in __getattr__ for details. 

3418 ''' 

3419 

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

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

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

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

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

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

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

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

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

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

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

3431 

3432 assert(not self.finished) 

3433 

3434 # check for mutual exclusiveness on input and output possibilities 

3435 assert(1 >= len( 

3436 [x for x in [ 

3437 in_stream, in_filename, in_string, in_columns, in_rows] 

3438 if x is not None])) 

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

3440 if x is not None])) 

3441 

3442 options = [] 

3443 

3444 gmt_config = self.gmt_config 

3445 if not self.is_gmt5(): 

3446 gmt_config_filename = self.gmt_config_filename 

3447 if config_override: 

3448 gmt_config = self.gmt_config.copy() 

3449 gmt_config.update(config_override) 

3450 gmt_config_override_filename = pjoin( 

3451 self.tempdir, 'gmtdefaults_override') 

3452 self.gen_gmt_config_file( 

3453 gmt_config_override_filename, gmt_config) 

3454 gmt_config_filename = gmt_config_override_filename 

3455 

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

3457 if config_override: 

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

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

3460 

3461 if out_discard: 

3462 out_filename = '/dev/null' 

3463 

3464 out_mustclose = False 

3465 if out_filename is not None: 

3466 out_mustclose = True 

3467 out_stream = open(out_filename, 'wb') 

3468 

3469 if in_filename is not None: 

3470 in_stream = open(in_filename, 'rb') 

3471 

3472 if in_string is not None: 

3473 in_stream = BytesIO(in_string) 

3474 

3475 encoding_gmt = gmt_config.get( 

3476 'PS_CHAR_ENCODING', 

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

3478 

3479 encoding = encoding_gmt_to_python[encoding_gmt.lower()] 

3480 

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

3482 in_stream = LineStreamChopper(TableLiner(in_columns=in_columns, 

3483 in_rows=in_rows, 

3484 encoding=encoding)) 

3485 

3486 # convert option arguments to strings 

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

3488 if len(k) > 1: 

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

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

3491 % (k, command)) 

3492 

3493 if type(v) is bool: 

3494 if v: 

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

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

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

3498 else: 

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

3500 

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

3502 if out_stream is None: 

3503 if not finish: 

3504 options.append('-K') 

3505 else: 

3506 self.finished = True 

3507 

3508 if not self.needstart: 

3509 options.append('-O') 

3510 else: 

3511 self.needstart = False 

3512 

3513 out_stream = self.output 

3514 

3515 # run the command 

3516 if self.is_gmt5(): 

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

3518 else: 

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

3520 

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

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

3523 args.extend(options) 

3524 args.extend(addargs) 

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

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

3527 args.append('+'+gmt_config_filename) 

3528 

3529 bs = 2048 

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

3531 stdout=subprocess.PIPE, bufsize=bs, 

3532 env=self.environ) 

3533 while True: 

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

3535 if cr: 

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

3537 if cw: 

3538 if in_stream is not None: 

3539 data = in_stream.read(bs) 

3540 if len(data) == 0: 

3541 break 

3542 p.stdin.write(data) 

3543 else: 

3544 break 

3545 if not cr and not cw: 

3546 break 

3547 

3548 p.stdin.close() 

3549 

3550 while True: 

3551 data = p.stdout.read(bs) 

3552 if len(data) == 0: 

3553 break 

3554 out_stream.write(data) 

3555 

3556 p.stdout.close() 

3557 

3558 retcode = p.wait() 

3559 

3560 if in_stream is not None: 

3561 in_stream.close() 

3562 

3563 if out_mustclose: 

3564 out_stream.close() 

3565 

3566 if retcode != 0: 

3567 self.keep_temp_dir = True 

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

3569 'While executing command:\n%s' 

3570 % (command, escape_shell_args(args))) 

3571 

3572 self.command_log.append(args) 

3573 

3574 def __getattr__(self, command): 

3575 

3576 ''' 

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

3578 

3579 Execute arbitrary GMT command. 

3580 

3581 Run a GMT command and by default append its postscript output to the 

3582 output file maintained by the GMT instance on which this method is 

3583 called. 

3584 

3585 Except for a few keyword arguments listed below, any ``kwargs`` and 

3586 ``addargs`` are converted into command line options and arguments and 

3587 passed to the GMT command. Numbers in keyword arguments are converted 

3588 into strings. E.g. ``S=10`` is translated into ``'-S10'``. Tuples of 

3589 numbers or strings are converted into strings where the elements of the 

3590 tuples are separated by slashes '/'. E.g. ``R=(10, 10, 20, 20)`` is 

3591 translated into ``'-R10/10/20/20'``. Options with a boolean argument 

3592 are only appended to the GMT command, if their values are True. 

3593 

3594 If no output redirection is in effect, the -K and -O options are 

3595 handled by gmtpy and thus should not be specified. Use 

3596 ``out_discard=True`` if you don't want -K or -O beeing added, but are 

3597 not interested in the output. 

3598 

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

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

3601 

3602 =============== ======================================================= 

3603 ``in_stream`` Data is read from an open file like object. 

3604 ``in_filename`` Data is read from the given file. 

3605 ``in_string`` String content is dumped to the process. 

3606 ``in_columns`` A 2D nested iterable whose elements can be accessed as 

3607 ``in_columns[icolumn][irow]`` is converted into an 

3608 ascii 

3609 table, which is fed to the process. 

3610 ``in_rows`` A 2D nested iterable whos elements can be accessed as 

3611 ``in_rows[irow][icolumn]`` is converted into an ascii 

3612 table, which is fed to the process. 

3613 =============== ======================================================= 

3614 

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

3616 following options: 

3617 

3618 ================= ===================================================== 

3619 ``out_stream`` Output is fed to an open file like object. 

3620 ``out_filename`` Output is dumped to the given file. 

3621 ``out_discard`` If True, output is dumped to :file:`/dev/null`. 

3622 ================= ===================================================== 

3623 

3624 Additional keyword arguments: 

3625 

3626 ===================== ================================================= 

3627 ``config`` Dict with GMT defaults which override the 

3628 currently active set of defaults exclusively 

3629 during this call. 

3630 ``finish`` If True, the postscript file, which is maintained 

3631 by the GMT instance is finished, and no further 

3632 plotting is allowed. 

3633 ``suppress_defaults`` Suppress appending of the ``'+gmtdefaults'`` 

3634 option to the command. 

3635 ===================== ================================================= 

3636 

3637 ''' 

3638 

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

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

3641 return f 

3642 

3643 def tempfilename(self, name=None): 

3644 ''' 

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

3646 

3647 If no ``name`` argument is given, a random name is picked. If 

3648 ``name`` is given, returns a path ending in that ``name``. 

3649 ''' 

3650 

3651 if not name: 

3652 name = ''.join( 

3653 [random.choice('abcdefghijklmnopqrstuvwxyz') 

3654 for i in range(10)]) 

3655 

3656 fn = pjoin(self.tempdir, name) 

3657 return fn 

3658 

3659 def tempfile(self, name=None): 

3660 ''' 

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

3662 ''' 

3663 

3664 fn = self.tempfilename(name) 

3665 f = open(fn, 'wb') 

3666 return f, fn 

3667 

3668 def save_unfinished(self, filename): 

3669 out = open(filename, 'wb') 

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

3671 out.close() 

3672 

3673 def load_unfinished(self, filename): 

3674 self.output = BytesIO() 

3675 self.finished = False 

3676 inp = open(filename, 'rb') 

3677 self.output.write(inp.read()) 

3678 inp.close() 

3679 

3680 def dump(self, ident): 

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

3682 self.save_unfinished(filename) 

3683 

3684 def load(self, ident): 

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

3686 self.load_unfinished(filename) 

3687 

3688 def save(self, filename=None, bbox=None, resolution=150, oversample=2., 

3689 width=None, height=None, size=None, crop_eps_mode=False, 

3690 psconvert=False): 

3691 

3692 ''' 

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

3694 

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

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

3697 

3698 If filename ends with ``'.png'`` a PNG file is created by running 

3699 :program:`gmtpy-epstopdf`, :program:`pdftocairo` and 

3700 :program:`convert`. ``resolution`` specifies the resolution in DPI for 

3701 raster file formats. Rasterization is done at a higher resolution if 

3702 ``oversample`` is set to a value higher than one. The output image size 

3703 can also be controlled by setting ``width``, ``height`` or ``size`` 

3704 instead of ``resolution``. When ``size`` is given, the image is scaled 

3705 so that ``max(width, height) == size``. 

3706 

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

3708 ''' 

3709 

3710 if not self.finished: 

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

3712 

3713 if filename: 

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

3715 out = open(tempfn, 'wb') 

3716 else: 

3717 out = sys.stdout 

3718 

3719 if bbox and not self.is_gmt5(): 

3720 out.write(replace_bbox(bbox, self.output.getvalue())) 

3721 else: 

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

3723 

3724 if filename: 

3725 out.close() 

3726 

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

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

3729 

3730 shutil.move(tempfn, filename) 

3731 return 

3732 

3733 if self.is_gmt5(): 

3734 if crop_eps_mode: 

3735 addarg = ['-A'] 

3736 else: 

3737 addarg = [] 

3738 

3739 subprocess.call( 

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

3741 '-Te', '-F%s' % tempfn + '.eps', tempfn, ] + addarg) 

3742 

3743 if bbox: 

3744 with open(tempfn + '.eps', 'rb') as fin: 

3745 with open(tempfn + '-fixbb.eps', 'wb') as fout: 

3746 replace_bbox(bbox, fin, fout) 

3747 

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

3749 

3750 else: 

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

3752 

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

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

3755 return 

3756 

3757 elif filename.endswith('.pdf'): 

3758 if psconvert: 

3759 gmt_bin = pjoin(self.installation['bin'], 'gmt') 

3760 subprocess.call([gmt_bin, 'psconvert', tempfn + '.eps', '-Tf', 

3761 '-F' + filename]) 

3762 else: 

3763 subprocess.call(['gmtpy-epstopdf', '--res=%i' % resolution, 

3764 '--outfile=' + filename, tempfn + '.eps']) 

3765 else: 

3766 subprocess.call([ 

3767 'gmtpy-epstopdf', 

3768 '--res=%i' % (resolution * oversample), 

3769 '--outfile=' + tempfn + '.pdf', tempfn + '.eps']) 

3770 

3771 convert_graph( 

3772 tempfn + '.pdf', filename, 

3773 resolution=resolution, oversample=oversample, 

3774 size=size, width=width, height=height) 

3775 

3776 def bbox(self): 

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

3778 

3779 def get_command_log(self): 

3780 ''' 

3781 Get the command log. 

3782 ''' 

3783 

3784 return self.command_log 

3785 

3786 def __str__(self): 

3787 s = '' 

3788 for com in self.command_log: 

3789 s += com[0] + "\n " + "\n ".join(com[1:]) + "\n\n" 

3790 return s 

3791 

3792 def page_size_points(self): 

3793 ''' 

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

3795 ''' 

3796 

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

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

3799 pm = pm[:-1] 

3800 

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

3802 

3803 if pm in all_paper_sizes(): 

3804 

3805 if orient == 'portrait': 

3806 return get_paper_size(pm) 

3807 else: 

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

3809 

3810 m = re.match(r'custom_([0-9.]+)([cimp]?)x([0-9.]+)([cimp]?)', pm) 

3811 if m: 

3812 w, uw, h, uh = m.groups() 

3813 w, h = float(w), float(h) 

3814 if uw: 

3815 w *= _units[uw] 

3816 if uh: 

3817 h *= _units[uh] 

3818 if orient == 'portrait': 

3819 return w, h 

3820 else: 

3821 return h, w 

3822 

3823 return None, None 

3824 

3825 def default_layout(self, with_palette=False): 

3826 ''' 

3827 Get a default layout for the output page. 

3828 

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

3830 `PAPER_MEDIA` setting in the GMT configuration dict. 

3831 

3832 If `PAPER_MEDIA` ends with a ``'+'`` (EPS output is selected), a 

3833 :py:class:`FrameLayout` is centered on the page, whose size is 

3834 controlled by its center widget's size plus the margins of the 

3835 :py:class:`FrameLayout`. 

3836 

3837 If `PAPER_MEDIA` indicates, that a custom page size is wanted by 

3838 starting with ``'Custom_'``, a :py:class:`FrameLayout` is used to fill 

3839 the complete page. The center widget's size is then controlled by the 

3840 page's size minus the margins of the :py:class:`FrameLayout`. 

3841 

3842 In any other case, two FrameLayouts are nested, such that the outer 

3843 layout attaches a 1 cm (printer) margin around the complete page, and 

3844 the inner FrameLayout's center widget takes up as much space as 

3845 possible under the constraint, that an aspect ratio of 1/golden_ratio 

3846 is preserved. 

3847 

3848 In any case, a reference to the innermost :py:class:`FrameLayout` 

3849 instance is returned. The top-level layout can be accessed by calling 

3850 :py:meth:`Widget.get_parent` on the returned layout. 

3851 ''' 

3852 

3853 if self.layout is None: 

3854 w, h = self.page_size_points() 

3855 

3856 if w is None or h is None: 

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

3858 

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

3860 

3861 if with_palette: 

3862 palette_layout = GridLayout(3, 1) 

3863 spacer = palette_layout.get_widget(1, 0) 

3864 palette_widget = palette_layout.get_widget(2, 0) 

3865 spacer.set_horizontal(0.5*cm) 

3866 palette_widget.set_horizontal(0.5*cm) 

3867 

3868 if pm.endswith('+') or self.eps_mode: 

3869 outer = CenterLayout() 

3870 outer.set_policy((w, h), (0., 0.)) 

3871 inner = FrameLayout() 

3872 outer.set_widget(inner) 

3873 if with_palette: 

3874 inner.set_widget('center', palette_layout) 

3875 widget = palette_layout 

3876 else: 

3877 widget = inner.get_widget('center') 

3878 widget.set_policy((w/golden_ratio, 0.), (0., 0.), 

3879 aspect=1./golden_ratio) 

3880 mw = 3.0*cm 

3881 inner.set_fixed_margins( 

3882 mw, mw, mw/golden_ratio, mw/golden_ratio) 

3883 self.layout = inner 

3884 

3885 elif pm.startswith('custom_'): 

3886 layout = FrameLayout() 

3887 layout.set_policy((w, h), (0., 0.)) 

3888 mw = 3.0*cm 

3889 layout.set_min_margins( 

3890 mw, mw, mw/golden_ratio, mw/golden_ratio) 

3891 if with_palette: 

3892 layout.set_widget('center', palette_layout) 

3893 self.layout = layout 

3894 else: 

3895 outer = FrameLayout() 

3896 outer.set_policy((w, h), (0., 0.)) 

3897 outer.set_fixed_margins(1.*cm, 1.*cm, 1.*cm, 1.*cm) 

3898 

3899 inner = FrameLayout() 

3900 outer.set_widget('center', inner) 

3901 mw = 3.0*cm 

3902 inner.set_min_margins(mw, mw, mw/golden_ratio, mw/golden_ratio) 

3903 if with_palette: 

3904 inner.set_widget('center', palette_layout) 

3905 widget = palette_layout 

3906 else: 

3907 widget = inner.get_widget('center') 

3908 

3909 widget.set_aspect(1./golden_ratio) 

3910 

3911 self.layout = inner 

3912 

3913 return self.layout 

3914 

3915 def draw_layout(self, layout): 

3916 ''' 

3917 Use psxy to draw layout; for debugging 

3918 ''' 

3919 

3920 # corners = layout.get_corners(descend=True) 

3921 rects = num.array(layout.get_sizes(), dtype=float) 

3922 rects_wid = rects[:, 0, 0] 

3923 rects_hei = rects[:, 0, 1] 

3924 rects_center_x = rects[:, 1, 0] + rects_wid*0.5 

3925 rects_center_y = rects[:, 1, 1] + rects_hei*0.5 

3926 nrects = len(rects) 

3927 prects = (rects_center_x, rects_center_y, num.arange(nrects), 

3928 num.zeros(nrects), rects_hei, rects_wid) 

3929 

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

3931 

3932 cptfile = self.tempfilename() + '.cpt' 

3933 self.makecpt( 

3934 C='ocean', 

3935 T='%g/%g/%g' % (-nrects, nrects, 1), 

3936 Z=True, 

3937 out_filename=cptfile, suppress_defaults=True) 

3938 

3939 bb = layout.bbox() 

3940 self.psxy( 

3941 in_columns=prects, 

3942 C=cptfile, 

3943 W='1p', 

3944 S='J', 

3945 R=(bb[0], bb[2], bb[1], bb[3]), 

3946 *layout.XYJ()) 

3947 

3948 

3949def simpleconf_to_ax(conf, axname): 

3950 c = {} 

3951 x = axname 

3952 for x in ('', axname): 

3953 for k in ('label', 'unit', 'scaled_unit', 'scaled_unit_factor', 

3954 'space', 'mode', 'approx_ticks', 'limits', 'masking', 'inc', 

3955 'snap'): 

3956 

3957 if x+k in conf: 

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

3959 

3960 return Ax(**c) 

3961 

3962 

3963class DensityPlotDef(object): 

3964 def __init__(self, data, cpt='ocean', tension=0.7, size=(640, 480), 

3965 contour=False, method='surface', zscaler=None, **extra): 

3966 self.data = data 

3967 self.cpt = cpt 

3968 self.tension = tension 

3969 self.size = size 

3970 self.contour = contour 

3971 self.method = method 

3972 self.zscaler = zscaler 

3973 self.extra = extra 

3974 

3975 

3976class TextDef(object): 

3977 def __init__( 

3978 self, 

3979 data, 

3980 size=9, 

3981 justify='MC', 

3982 fontno=0, 

3983 offset=(0, 0), 

3984 color='black'): 

3985 

3986 self.data = data 

3987 self.size = size 

3988 self.justify = justify 

3989 self.fontno = fontno 

3990 self.offset = offset 

3991 self.color = color 

3992 

3993 

3994class Simple(object): 

3995 def __init__(self, gmtconfig=None, gmtversion='newest', **simple_config): 

3996 self.data = [] 

3997 self.symbols = [] 

3998 self.config = copy.deepcopy(simple_config) 

3999 self.gmtconfig = gmtconfig 

4000 self.density_plot_defs = [] 

4001 self.text_defs = [] 

4002 

4003 self.gmtversion = gmtversion 

4004 

4005 self.data_x = [] 

4006 self.symbols_x = [] 

4007 

4008 self.data_y = [] 

4009 self.symbols_y = [] 

4010 

4011 self.default_config = {} 

4012 self.set_defaults(width=15.*cm, 

4013 height=15.*cm / golden_ratio, 

4014 margins=(2.*cm, 2.*cm, 2.*cm, 2.*cm), 

4015 with_palette=False, 

4016 palette_offset=0.5*cm, 

4017 palette_width=None, 

4018 palette_height=None, 

4019 zlabeloffset=2*cm, 

4020 draw_layout=False) 

4021 

4022 self.setup_defaults() 

4023 self.fixate_widget_aspect = False 

4024 

4025 def setup_defaults(self): 

4026 pass 

4027 

4028 def set_defaults(self, **kwargs): 

4029 self.default_config.update(kwargs) 

4030 

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

4032 self.data.append(data) 

4033 self.symbols.append(symbol) 

4034 

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

4036 dpd = DensityPlotDef(data, **kwargs) 

4037 self.density_plot_defs.append(dpd) 

4038 

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

4040 dpd = TextDef(data, **kwargs) 

4041 self.text_defs.append(dpd) 

4042 

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

4044 self.data_x.append(data) 

4045 self.symbols_x.append(symbol) 

4046 

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

4048 self.data_y.append(data) 

4049 self.symbols_y.append(symbol) 

4050 

4051 def set(self, **kwargs): 

4052 self.config.update(kwargs) 

4053 

4054 def setup_base(self, conf): 

4055 w = conf.pop('width') 

4056 h = conf.pop('height') 

4057 margins = conf.pop('margins') 

4058 

4059 gmtconfig = {} 

4060 if self.gmtconfig is not None: 

4061 gmtconfig.update(self.gmtconfig) 

4062 

4063 gmt = GMT( 

4064 version=self.gmtversion, 

4065 config=gmtconfig, 

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

4067 

4068 layout = gmt.default_layout(with_palette=conf['with_palette']) 

4069 layout.set_min_margins(*margins) 

4070 if conf['with_palette']: 

4071 widget = layout.get_widget().get_widget(0, 0) 

4072 spacer = layout.get_widget().get_widget(1, 0) 

4073 spacer.set_horizontal(conf['palette_offset']) 

4074 palette_widget = layout.get_widget().get_widget(2, 0) 

4075 if conf['palette_width'] is not None: 

4076 palette_widget.set_horizontal(conf['palette_width']) 

4077 if conf['palette_height'] is not None: 

4078 palette_widget.set_vertical(conf['palette_height']) 

4079 widget.set_vertical(h-margins[2]-margins[3]-0.03*cm) 

4080 return gmt, layout, widget, palette_widget 

4081 else: 

4082 widget = layout.get_widget() 

4083 return gmt, layout, widget, None 

4084 

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

4086 pass 

4087 

4088 def setup_scaling(self, conf): 

4089 ndims = 2 

4090 if self.density_plot_defs: 

4091 ndims = 3 

4092 

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

4094 

4095 data_all = [] 

4096 data_all.extend(self.data) 

4097 for dsd in self.density_plot_defs: 

4098 if dsd.zscaler is None: 

4099 data_all.append(dsd.data) 

4100 else: 

4101 data_all.append(dsd.data[:2]) 

4102 data_chopped = [ds[:ndims] for ds in data_all] 

4103 

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

4105 

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

4107 

4108 return scaler 

4109 

4110 def setup_scaling_plus(self, scaler, axes): 

4111 pass 

4112 

4113 def setup_scaling_extra(self, scaler, conf): 

4114 

4115 scaler_x = scaler.copy() 

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

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

4118 

4119 scaler_y = scaler.copy() 

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

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

4122 

4123 return scaler_x, scaler_y 

4124 

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

4126 

4127 R = scaler.R() 

4128 # par = scaler.get_params() 

4129 rxyj = R + widget.XYJ() 

4130 innerticks = False 

4131 for dpd in self.density_plot_defs: 

4132 

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

4134 

4135 if dpd.zscaler is not None: 

4136 s = dpd.zscaler 

4137 else: 

4138 s = scaler 

4139 

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

4141 

4142 fn_grid = gmt.tempfilename() 

4143 

4144 fn_mean = gmt.tempfilename() 

4145 

4146 if dpd.method in ('surface', 'triangulate'): 

4147 gmt.blockmean(in_columns=dpd.data, 

4148 I='%i+/%i+' % dpd.size, # noqa 

4149 out_filename=fn_mean, *R) 

4150 

4151 if dpd.method == 'surface': 

4152 gmt.surface( 

4153 in_filename=fn_mean, 

4154 T=dpd.tension, 

4155 G=fn_grid, 

4156 I='%i+/%i+' % dpd.size, # noqa 

4157 out_discard=True, 

4158 *R) 

4159 

4160 if dpd.method == 'triangulate': 

4161 gmt.triangulate( 

4162 in_filename=fn_mean, 

4163 G=fn_grid, 

4164 I='%i+/%i+' % dpd.size, # noqa 

4165 out_discard=True, 

4166 V=True, 

4167 *R) 

4168 

4169 if gmt.is_gmt5(): 

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

4171 

4172 else: 

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

4174 

4175 if dpd.contour: 

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

4177 innerticks = '0.5p,black' 

4178 

4179 os.remove(fn_grid) 

4180 os.remove(fn_mean) 

4181 

4182 if dpd.method == 'fillcontour': 

4183 extra = dict(C=fn_cpt) 

4184 extra.update(dpd.extra) 

4185 gmt.pscontour(in_columns=dpd.data, 

4186 I=True, *rxyj, **extra) # noqa 

4187 

4188 if dpd.method == 'contour': 

4189 extra = dict(W='0.5p,black', C=fn_cpt) 

4190 extra.update(dpd.extra) 

4191 gmt.pscontour(in_columns=dpd.data, *rxyj, **extra) 

4192 

4193 return fn_cpt, innerticks 

4194 

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

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

4197 

4198 def draw(self, gmt, widget, scaler): 

4199 rxyj = scaler.R() + widget.JXY() 

4200 for dat, sym in zip(self.data, self.symbols): 

4201 gmt.psxy(in_columns=dat, *(sym.split()+rxyj)) 

4202 

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

4204 pass 

4205 

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

4207 pass 

4208 

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

4210 

4211 for dat, sym in zip(self.data_x, self.symbols_x): 

4212 gmt.psxy(in_columns=dat, 

4213 *(sym.split() + scaler_x.R() + widget.JXY())) 

4214 

4215 for dat, sym in zip(self.data_y, self.symbols_y): 

4216 gmt.psxy(in_columns=dat, 

4217 *(sym.split() + scaler_y.R() + widget.JXY())) 

4218 

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

4220 

4221 rxyj = scaler.R() + widget.JXY() 

4222 for td in self.text_defs: 

4223 x, y = td.data[0:2] 

4224 text = td.data[-1] 

4225 size = td.size 

4226 angle = 0 

4227 fontno = td.fontno 

4228 justify = td.justify 

4229 color = td.color 

4230 if gmt.is_gmt5(): 

4231 gmt.pstext( 

4232 in_rows=[(x, y, text)], 

4233 F='+f%gp,%s,%s+a%g+j%s' % ( 

4234 size, fontno, color, angle, justify), 

4235 D='%gp/%gp' % td.offset, *rxyj) 

4236 else: 

4237 gmt.pstext( 

4238 in_rows=[(x, y, size, angle, fontno, justify, text)], 

4239 D='%gp/%gp' % td.offset, *rxyj) 

4240 

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

4242 

4243 conf = dict(self.default_config) 

4244 conf.update(self.config) 

4245 

4246 gmt, layout, widget, palette_widget = self.setup_base(conf) 

4247 scaler = self.setup_scaling(conf) 

4248 scaler_x, scaler_y = self.setup_scaling_extra(scaler, conf) 

4249 

4250 self.setup_projection(widget, scaler, conf) 

4251 if self.fixate_widget_aspect: 

4252 aspect = aspect_for_projection( 

4253 gmt.installation['version'], *(widget.J() + scaler.R())) 

4254 

4255 widget.set_aspect(aspect) 

4256 

4257 if conf['draw_layout']: 

4258 gmt.draw_layout(layout) 

4259 cptfile = None 

4260 if self.density_plot_defs: 

4261 cptfile, innerticks = self.draw_density(gmt, widget, scaler) 

4262 self.pre_draw(gmt, widget, scaler) 

4263 self.draw(gmt, widget, scaler) 

4264 self.post_draw(gmt, widget, scaler) 

4265 self.draw_extra(gmt, widget, scaler_x, scaler_y) 

4266 self.draw_text(gmt, widget, scaler) 

4267 self.draw_basemap(gmt, widget, scaler) 

4268 

4269 if palette_widget and cptfile: 

4270 nice_palette(gmt, palette_widget, scaler, cptfile, 

4271 innerticks=innerticks, 

4272 zlabeloffset=conf['zlabeloffset']) 

4273 

4274 gmt.save(filename, resolution=resolution) 

4275 

4276 

4277class LinLinPlot(Simple): 

4278 pass 

4279 

4280 

4281class LogLinPlot(Simple): 

4282 

4283 def setup_defaults(self): 

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

4285 

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

4287 widget['J'] = '-JX%(width)gpl/%(height)gp' 

4288 scaler['B'] = '-B2:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen' 

4289 

4290 

4291class LinLogPlot(Simple): 

4292 

4293 def setup_defaults(self): 

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

4295 

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

4297 widget['J'] = '-JX%(width)gp/%(height)gpl' 

4298 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/2:%(ylabel)s:WSen' 

4299 

4300 

4301class LogLogPlot(Simple): 

4302 

4303 def setup_defaults(self): 

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

4305 

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

4307 widget['J'] = '-JX%(width)gpl/%(height)gpl' 

4308 scaler['B'] = '-B2:%(xlabel)s:/2:%(ylabel)s:WSen' 

4309 

4310 

4311class AziDistPlot(Simple): 

4312 

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

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

4315 self.fixate_widget_aspect = True 

4316 

4317 def setup_defaults(self): 

4318 self.set_defaults( 

4319 height=15.*cm, 

4320 width=15.*cm, 

4321 xmode='off', 

4322 xlimits=(0., 360.), 

4323 xinc=45.) 

4324 

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

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

4327 

4328 def setup_scaling_plus(self, scaler, axes): 

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

4330 

4331 

4332class MPlot(Simple): 

4333 

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

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

4336 self.fixate_widget_aspect = True 

4337 

4338 def setup_defaults(self): 

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

4340 

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

4342 par = scaler.get_params() 

4343 lon0 = (par['xmin'] + par['xmax'])/2. 

4344 lat0 = (par['ymin'] + par['ymax'])/2. 

4345 sll = '%g/%g' % (lon0, lat0) 

4346 widget['J'] = '-JM' + sll + '/%(width)gp' 

4347 scaler['B'] = \ 

4348 '-B%(xinc)gg%(xinc)g:%(xlabel)s:/%(yinc)gg%(yinc)g:%(ylabel)s:WSen' 

4349 

4350 

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

4352 innerticks=True): 

4353 

4354 par = scaleguru.get_params() 

4355 par_ax = scaleguru.get_params(ax_projection=True) 

4356 nz_palette = int(widget.height()/inch * 300) 

4357 px = num.zeros(nz_palette*2) 

4358 px[1::2] += 1 

4359 pz = num.linspace(par['zmin'], par['zmax'], nz_palette).repeat(2) 

4360 pdz = pz[2]-pz[0] 

4361 palgrdfile = gmt.tempfilename() 

4362 pal_r = (0, 1, par['zmin'], par['zmax']) 

4363 pal_ax_r = (0, 1, par_ax['zmin'], par_ax['zmax']) 

4364 gmt.xyz2grd( 

4365 G=palgrdfile, R=pal_r, 

4366 I=(1, pdz), in_columns=(px, pz, pz), # noqa 

4367 out_discard=True) 

4368 

4369 gmt.grdimage(palgrdfile, R=pal_r, C=cptfile, *widget.JXY()) 

4370 if isinstance(innerticks, str): 

4371 tickpen = innerticks 

4372 gmt.grdcontour(palgrdfile, W=tickpen, R=pal_r, C=cptfile, 

4373 *widget.JXY()) 

4374 

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

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

4377 ticklen = negpalwid 

4378 else: 

4379 ticklen = '0p' 

4380 

4381 TICK_LENGTH_PARAM = 'MAP_TICK_LENGTH' if gmt.is_gmt5() else 'TICK_LENGTH' 

4382 gmt.psbasemap( 

4383 R=pal_ax_r, B='4::/%(zinc)g::nsw' % par_ax, 

4384 config={TICK_LENGTH_PARAM: ticklen}, 

4385 *widget.JXY()) 

4386 

4387 if innerticks: 

4388 gmt.psbasemap( 

4389 R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax, 

4390 config={TICK_LENGTH_PARAM: '0p'}, 

4391 *widget.JXY()) 

4392 else: 

4393 gmt.psbasemap(R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax, *widget.JXY()) 

4394 

4395 if par_ax['zlabel']: 

4396 label_font = gmt.label_font() 

4397 label_font_size = gmt.label_font_size() 

4398 label_offset = zlabeloffset 

4399 gmt.pstext( 

4400 R=(0, 1, 0, 2), D="%gp/0p" % label_offset, 

4401 N=True, 

4402 in_rows=[(1, 1, label_font_size, -90, label_font, 'CB', 

4403 par_ax['zlabel'])], 

4404 *widget.JXY())