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 x = to_array(nc.variables[kx]) 

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

1542 z = to_array(nc.variables['z']) 

1543 

1544 nc.close() 

1545 return x, y, z 

1546 

1547 

1548def centers_to_edges(asorted): 

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

1550 

1551 

1552def nvals(asorted): 

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

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

1555 

1556 

1557def guess_vals(asorted): 

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

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

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

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

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

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

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

1565 

1566 

1567def blockmean(asorted, b): 

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

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

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

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

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

1573 return ( 

1574 asorted[indis[:-1]], 

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

1576 

1577 

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

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

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

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

1582 

1583 zindi = yindi*nx+xindi 

1584 order = num.argsort(zindi) 

1585 z = z[order] 

1586 zindi = zindi[order] 

1587 

1588 zindi, z = blockmean(zindi, z) 

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

1590 znew[:] = num.nan 

1591 znew[zindi] = z 

1592 return znew.reshape(ny, nx) 

1593 

1594 

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

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

1597 xs = x_sorted 

1598 ys = y_sorted 

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

1600 if mode == 'nonrandom': 

1601 return nxs, nys, 0 

1602 elif xs.size == nxs*nys: 

1603 # exact match 

1604 return nxs, nys, 0 

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

1606 # possibly randomly sampled 

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

1608 nys = nxs 

1609 return nxs, nys, 2 

1610 else: 

1611 return nxs, nys, 1 

1612 

1613 

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

1615 ''' 

1616 Grid tabular XYZ data by binning. 

1617 

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

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

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

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

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

1623 

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

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

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

1627 ''' 

1628 

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

1630 assert x.size == y.size == z.size 

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

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

1633 if badness <= 1: 

1634 xf = guess_vals(xs) 

1635 yf = guess_vals(ys) 

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

1637 else: 

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

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

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

1641 

1642 return xf, yf, zf 

1643 

1644 

1645def tabledata(xf, yf, zf): 

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

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

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

1649 z = zf.flatten() 

1650 return x, y, z 

1651 

1652 

1653def double1d(a): 

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

1655 a2[::2] = a 

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

1657 return a2 

1658 

1659 

1660def double2d(f): 

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

1662 f2[:, :] = num.nan 

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

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

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

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

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

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

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

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

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

1672 return f2 

1673 

1674 

1675def doublegrid(x, y, z): 

1676 x2 = double1d(x) 

1677 y2 = double1d(y) 

1678 z2 = double2d(z) 

1679 return x2, y2, z2 

1680 

1681 

1682class Guru(object): 

1683 ''' 

1684 Abstract base class providing template interpolation, accessible as 

1685 attributes. 

1686 

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

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

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

1690 with the templates. 

1691 ''' 

1692 

1693 def __init__(self): 

1694 self.templates = {} 

1695 

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

1697 params = self.get_params(**kwargs) 

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

1699 return strings 

1700 

1701 # hand through templates dict 

1702 def __getitem__(self, template_name): 

1703 return self.templates[template_name] 

1704 

1705 def __setitem__(self, template_name, template): 

1706 self.templates[template_name] = template 

1707 

1708 def __contains__(self, template_name): 

1709 return template_name in self.templates 

1710 

1711 def __iter__(self): 

1712 return iter(self.templates) 

1713 

1714 def __len__(self): 

1715 return len(self.templates) 

1716 

1717 def __delitem__(self, template_name): 

1718 del(self.templates[template_name]) 

1719 

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

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

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

1723 

1724 def __getattr__(self, template_names): 

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

1726 raise AttributeError(template_names) 

1727 

1728 def f(**kwargs): 

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

1730 

1731 return f 

1732 

1733 

1734def nice_value(x): 

1735 ''' 

1736 Round ``x`` to nice value. 

1737 ''' 

1738 

1739 exp = 1.0 

1740 sign = 1 

1741 if x < 0.0: 

1742 x = -x 

1743 sign = -1 

1744 while x >= 1.0: 

1745 x /= 10.0 

1746 exp *= 10.0 

1747 while x < 0.1: 

1748 x *= 10.0 

1749 exp /= 10.0 

1750 

1751 if x >= 0.75: 

1752 return sign * 1.0 * exp 

1753 if x >= 0.375: 

1754 return sign * 0.5 * exp 

1755 if x >= 0.225: 

1756 return sign * 0.25 * exp 

1757 if x >= 0.15: 

1758 return sign * 0.2 * exp 

1759 

1760 return sign * 0.1 * exp 

1761 

1762 

1763class AutoScaler(object): 

1764 ''' 

1765 Tunable 1D autoscaling based on data range. 

1766 

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

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

1769 notation. 

1770 

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

1772 

1773 .. py:attribute:: approx_ticks 

1774 

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

1776 

1777 .. py:attribute:: mode 

1778 

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

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

1781 

1782 ================ ================================================== 

1783 mode description 

1784 ================ ================================================== 

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

1786 below. 

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

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

1789 max. 

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

1791 zero. 

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

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

1794 disabled, such that the output range always 

1795 exactly matches the data range. 

1796 ================ ================================================== 

1797 

1798 .. py:attribute:: exp 

1799 

1800 If defined, override automatically determined exponent for notation 

1801 by the given value. 

1802 

1803 .. py:attribute:: snap 

1804 

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

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

1807 

1808 .. py:attribute:: inc 

1809 

1810 If defined, override automatically determined tick increment by the 

1811 given value. 

1812 

1813 .. py:attribute:: space 

1814 

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

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

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

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

1819 

1820 .. py:attribute:: exp_factor 

1821 

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

1823 

1824 .. py:attribute:: no_exp_interval: 

1825 

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

1827 

1828 ''' 

1829 

1830 def __init__( 

1831 self, 

1832 approx_ticks=7.0, 

1833 mode='auto', 

1834 exp=None, 

1835 snap=False, 

1836 inc=None, 

1837 space=0.0, 

1838 exp_factor=3, 

1839 no_exp_interval=(-3, 5)): 

1840 

1841 ''' 

1842 Create new AutoScaler instance. 

1843 

1844 The parameters are described in the AutoScaler documentation. 

1845 ''' 

1846 

1847 self.approx_ticks = approx_ticks 

1848 self.mode = mode 

1849 self.exp = exp 

1850 self.snap = snap 

1851 self.inc = inc 

1852 self.space = space 

1853 self.exp_factor = exp_factor 

1854 self.no_exp_interval = no_exp_interval 

1855 

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

1857 

1858 ''' 

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

1860 

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

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

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

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

1865 value. 

1866 ''' 

1867 

1868 data_min = min(data_range) 

1869 data_max = max(data_range) 

1870 

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

1872 

1873 a = self.mode 

1874 if self.mode == 'auto': 

1875 a = self.guess_autoscale_mode(data_min, data_max) 

1876 

1877 if override_mode is not None: 

1878 a = override_mode 

1879 

1880 mi, ma = 0, 0 

1881 if a == 'off': 

1882 mi, ma = data_min, data_max 

1883 elif a == '0-max': 

1884 mi = 0.0 

1885 if data_max > 0.0: 

1886 ma = data_max 

1887 else: 

1888 ma = 1.0 

1889 elif a == 'min-0': 

1890 ma = 0.0 

1891 if data_min < 0.0: 

1892 mi = data_min 

1893 else: 

1894 mi = -1.0 

1895 elif a == 'min-max': 

1896 mi, ma = data_min, data_max 

1897 elif a == 'symmetric': 

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

1899 mi = -m 

1900 ma = m 

1901 

1902 nmi = mi 

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

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

1905 

1906 nma = ma 

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

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

1909 

1910 mi, ma = nmi, nma 

1911 

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

1913 mi -= 1.0 

1914 ma += 1.0 

1915 

1916 # make nice tick increment 

1917 if self.inc is not None: 

1918 inc = self.inc 

1919 else: 

1920 if self.approx_ticks > 0.: 

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

1922 else: 

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

1924 

1925 if inc == 0.0: 

1926 inc = 1.0 

1927 

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

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

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

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

1932 

1933 if is_reverse: 

1934 return ma, mi, -inc 

1935 else: 

1936 return mi, ma, inc 

1937 

1938 def make_exp(self, x): 

1939 ''' 

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

1941 

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

1943 ''' 

1944 

1945 if self.exp is not None: 

1946 return self.exp 

1947 

1948 x = abs(x) 

1949 if x == 0.0: 

1950 return 0 

1951 

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

1953 return 0 

1954 

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

1956 

1957 def guess_autoscale_mode(self, data_min, data_max): 

1958 ''' 

1959 Guess mode of operation, based on data range. 

1960 

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

1962 or ``'symmetric'``. 

1963 ''' 

1964 

1965 a = 'min-max' 

1966 if data_min >= 0.0: 

1967 if data_min < data_max/2.: 

1968 a = '0-max' 

1969 else: 

1970 a = 'min-max' 

1971 if data_max <= 0.0: 

1972 if data_max > data_min/2.: 

1973 a = 'min-0' 

1974 else: 

1975 a = 'min-max' 

1976 if data_min < 0.0 and data_max > 0.0: 

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

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

1979 a = 'symmetric' 

1980 else: 

1981 a = 'min-max' 

1982 return a 

1983 

1984 

1985class Ax(AutoScaler): 

1986 ''' 

1987 Ax description with autoscaling capabilities. 

1988 

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

1990 the following additional attributes (with default values given in 

1991 paranthesis): 

1992 

1993 .. py:attribute:: label 

1994 

1995 Ax label (without unit). 

1996 

1997 .. py:attribute:: unit 

1998 

1999 Physical unit of the data attached to this ax. 

2000 

2001 .. py:attribute:: scaled_unit 

2002 

2003 (see below) 

2004 

2005 .. py:attribute:: scaled_unit_factor 

2006 

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

2008 

2009 unit = scaled_unit_factor x scaled_unit. 

2010 

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

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

2013 1e9.) 

2014 

2015 .. py:attribute:: limits 

2016 

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

2018 

2019 .. py:attribute:: masking 

2020 

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

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

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

2024 

2025 ''' 

2026 

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

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

2029 

2030 AutoScaler.__init__(self, **kwargs) 

2031 self.label = label 

2032 self.unit = unit 

2033 self.scaled_unit_factor = scaled_unit_factor 

2034 self.scaled_unit = scaled_unit 

2035 self.limits = limits 

2036 self.masking = masking 

2037 

2038 def label_str(self, exp, unit): 

2039 ''' 

2040 Get label string including the unit and multiplier. 

2041 ''' 

2042 

2043 slabel, sunit, sexp = '', '', '' 

2044 if self.label: 

2045 slabel = self.label 

2046 

2047 if unit or exp != 0: 

2048 if exp != 0: 

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

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

2051 else: 

2052 sunit = '[ %s ]' % unit 

2053 

2054 p = [] 

2055 if slabel: 

2056 p.append(slabel) 

2057 

2058 if sunit: 

2059 p.append(sunit) 

2060 

2061 return ' '.join(p) 

2062 

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

2064 override_scaled_unit_factor=None): 

2065 

2066 ''' 

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

2068 

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

2070 multiplier for given data range. 

2071 

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

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

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

2075 scaling applied. 

2076 ''' 

2077 

2078 sf = self.scaled_unit_factor 

2079 

2080 if override_scaled_unit_factor is not None: 

2081 sf = override_scaled_unit_factor 

2082 

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

2084 

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

2086 if self.inc is not None: 

2087 inc = self.inc*sf 

2088 

2089 if ax_projection: 

2090 exp = self.make_exp(inc) 

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

2092 unit = self.unit 

2093 else: 

2094 unit = self.scaled_unit 

2095 label = self.label_str(exp, unit) 

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

2097 else: 

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

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

2100 

2101 

2102class ScaleGuru(Guru): 

2103 

2104 ''' 

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

2106 

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

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

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

2110 arguments, which are required for most GMT commands. 

2111 

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

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

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

2115 

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

2117 

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

2119 limits imposed on other axes. 

2120 

2121 ''' 

2122 

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

2124 percent_interval=None, copy_from=None): 

2125 

2126 Guru.__init__(self) 

2127 

2128 if copy_from: 

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

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

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

2132 self.aspect = copy_from.aspect 

2133 

2134 if percent_interval is not None: 

2135 from scipy.stats import scoreatpercentile as scap 

2136 

2137 self.templates = dict( 

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

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

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

2141 

2142 maxdim = 2 

2143 if data_tuples: 

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

2145 else: 

2146 if axes: 

2147 maxdim = len(axes) 

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

2149 if axes is not None: 

2150 self.axes = axes 

2151 else: 

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

2153 

2154 # sophisticated data-range calculation 

2155 data_ranges = [None] * maxdim 

2156 for dt_ in data_tuples: 

2157 dt = num.asarray(dt_) 

2158 in_range = True 

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

2160 if ax.limits and ax.masking: 

2161 ax_limits = list(ax.limits) 

2162 if ax_limits[0] is None: 

2163 ax_limits[0] = -num.inf 

2164 if ax_limits[1] is None: 

2165 ax_limits[1] = num.inf 

2166 in_range = num.logical_and( 

2167 in_range, 

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

2169 

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

2171 

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

2173 if len(x) >= 1: 

2174 if in_range is not True: 

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

2176 if percent_interval is None: 

2177 range_this = ( 

2178 num.nanmin(xmasked), 

2179 num.nanmax(xmasked)) 

2180 else: 

2181 xmasked_finite = num.compress( 

2182 num.isfinite(xmasked), xmasked) 

2183 range_this = ( 

2184 scap(xmasked_finite, 

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

2186 scap(xmasked_finite, 

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

2188 else: 

2189 if percent_interval is None: 

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

2191 else: 

2192 xmasked_finite = num.compress( 

2193 num.isfinite(xmasked), xmasked) 

2194 range_this = ( 

2195 scap(xmasked_finite, 

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

2197 scap(xmasked_finite, 

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

2199 else: 

2200 range_this = (0., 1.) 

2201 

2202 if ax.limits: 

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

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

2205 range_this[1]) 

2206 

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

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

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

2210 

2211 else: 

2212 range_this = ax.limits 

2213 

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

2215 data_ranges[i] = range_this 

2216 else: 

2217 mi, ma = range_this 

2218 if data_ranges[i] is not None: 

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

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

2221 

2222 data_ranges[i] = (mi, ma) 

2223 

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

2225 if data_ranges[i] is None or not ( 

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

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

2228 

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

2230 

2231 self.data_ranges = data_ranges 

2232 self.aspect = aspect 

2233 

2234 def copy(self): 

2235 return ScaleGuru(copy_from=self) 

2236 

2237 def get_params(self, ax_projection=False): 

2238 

2239 ''' 

2240 Get dict with output parameters. 

2241 

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

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

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

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

2246 

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

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

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

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

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

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

2253 label string. 

2254 ''' 

2255 

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

2257 self.data_ranges[0], ax_projection) 

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

2259 self.data_ranges[1], ax_projection) 

2260 if len(self.axes) > 2: 

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

2262 self.data_ranges[2], ax_projection) 

2263 

2264 # enforce certain aspect, if needed 

2265 if self.aspect is not None: 

2266 xwid = xma-xmi 

2267 ywid = yma-ymi 

2268 if ywid < xwid*self.aspect: 

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

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

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

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

2273 override_scaled_unit_factor=1.) 

2274 

2275 elif xwid < ywid/self.aspect: 

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

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

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

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

2280 override_scaled_unit_factor=1.) 

2281 

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

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

2284 if len(self.axes) > 2: 

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

2286 

2287 return params 

2288 

2289 

2290class GumSpring(object): 

2291 

2292 ''' 

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

2294 ''' 

2295 

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

2297 self.minimal = minimal 

2298 if grow is None: 

2299 if minimal is None: 

2300 self.grow = 1.0 

2301 else: 

2302 self.grow = 0.0 

2303 else: 

2304 self.grow = grow 

2305 self.value = 1.0 

2306 

2307 def get_minimal(self): 

2308 if self.minimal is not None: 

2309 return self.minimal 

2310 else: 

2311 return 0.0 

2312 

2313 def get_grow(self): 

2314 return self.grow 

2315 

2316 def set_value(self, value): 

2317 self.value = value 

2318 

2319 def get_value(self): 

2320 return self.value 

2321 

2322 

2323def distribute(sizes, grows, space): 

2324 sizes = list(sizes) 

2325 gsum = sum(grows) 

2326 if gsum > 0.0: 

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

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

2329 return sizes 

2330 

2331 

2332class Widget(Guru): 

2333 

2334 ''' 

2335 Base class of the gmtpy layout system. 

2336 

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

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

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

2340 

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

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

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

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

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

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

2347 

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

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

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

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

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

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

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

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

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

2357 

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

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

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

2361 ''' 

2362 

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

2364 

2365 ''' 

2366 Create new widget. 

2367 ''' 

2368 

2369 Guru.__init__(self) 

2370 

2371 self.templates = dict( 

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

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

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

2375 

2376 if horizontal is None: 

2377 self.horizontal = GumSpring() 

2378 else: 

2379 self.horizontal = horizontal 

2380 

2381 if vertical is None: 

2382 self.vertical = GumSpring() 

2383 else: 

2384 self.vertical = vertical 

2385 

2386 self.aspect = None 

2387 self.parent = parent 

2388 self.dirty = True 

2389 

2390 def set_parent(self, parent): 

2391 

2392 ''' 

2393 Set the parent widget. 

2394 

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

2396 methods are responsible for calling this. 

2397 ''' 

2398 

2399 self.parent = parent 

2400 self.dirtyfy() 

2401 

2402 def get_parent(self): 

2403 

2404 ''' 

2405 Get the widgets parent widget. 

2406 ''' 

2407 

2408 return self.parent 

2409 

2410 def get_root(self): 

2411 

2412 ''' 

2413 Get the root widget in the layout hierarchy. 

2414 ''' 

2415 

2416 if self.parent is not None: 

2417 return self.get_parent() 

2418 else: 

2419 return self 

2420 

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

2422 

2423 ''' 

2424 Set the horizontal sizing policy of the Widget. 

2425 

2426 

2427 :param minimal: new minimal width of the widget 

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

2429 ''' 

2430 

2431 self.horizontal = GumSpring(minimal, grow) 

2432 self.dirtyfy() 

2433 

2434 def get_horizontal(self): 

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

2436 

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

2438 

2439 ''' 

2440 Set the horizontal sizing policy of the Widget. 

2441 

2442 :param minimal: new minimal height of the widget 

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

2444 ''' 

2445 

2446 self.vertical = GumSpring(minimal, grow) 

2447 self.dirtyfy() 

2448 

2449 def get_vertical(self): 

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

2451 

2452 def set_aspect(self, aspect=None): 

2453 

2454 ''' 

2455 Set aspect constraint on the widget. 

2456 

2457 The aspect is given as height divided by width. 

2458 ''' 

2459 

2460 self.aspect = aspect 

2461 self.dirtyfy() 

2462 

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

2464 

2465 ''' 

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

2467 call. 

2468 ''' 

2469 

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

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

2472 self.set_aspect(aspect) 

2473 

2474 def get_policy(self): 

2475 mh, gh = self.get_horizontal() 

2476 mv, gv = self.get_vertical() 

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

2478 

2479 def legalize(self, size, offset): 

2480 

2481 ''' 

2482 Get legal size for widget. 

2483 

2484 Returns: (new_size, new_offset) 

2485 

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

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

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

2489 ''' 

2490 

2491 sh, sv = size 

2492 oh, ov = offset 

2493 shs, svs = Widget.get_min_size(self) 

2494 ghs, gvs = Widget.get_grow(self) 

2495 

2496 if ghs == 0.0: 

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

2498 sh = shs 

2499 

2500 if gvs == 0.0: 

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

2502 sv = svs 

2503 

2504 if self.aspect is not None: 

2505 if sh > sv/self.aspect: 

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

2507 sh = sv/self.aspect 

2508 if sv > sh*self.aspect: 

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

2510 sv = sh*self.aspect 

2511 

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

2513 

2514 def get_min_size(self): 

2515 

2516 ''' 

2517 Get minimum size of widget. 

2518 

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

2520 ''' 

2521 

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

2523 if self.aspect is not None: 

2524 if mv == 0.0: 

2525 return mh, mh*self.aspect 

2526 elif mh == 0.0: 

2527 return mv/self.aspect, mv 

2528 return mh, mv 

2529 

2530 def get_grow(self): 

2531 

2532 ''' 

2533 Get widget's desire to grow. 

2534 

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

2536 ''' 

2537 

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

2539 

2540 def set_size(self, size, offset): 

2541 

2542 ''' 

2543 Set the widget's current size. 

2544 

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

2546 responsibility to call this. 

2547 ''' 

2548 

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

2550 self.offset = inner_offset 

2551 self.horizontal.set_value(sh) 

2552 self.vertical.set_value(sv) 

2553 self.dirty = False 

2554 

2555 def __str__(self): 

2556 

2557 def indent(ind, str): 

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

2559 size, offset = self.get_size() 

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

2561 children = self.get_children() 

2562 if children: 

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

2564 return s 

2565 

2566 def policies_debug_str(self): 

2567 

2568 def indent(ind, str): 

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

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

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

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

2573 

2574 children = self.get_children() 

2575 if children: 

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

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

2578 return s 

2579 

2580 def get_corners(self, descend=False): 

2581 

2582 ''' 

2583 Get coordinates of the corners of the widget. 

2584 

2585 Returns list with coordinate tuples. 

2586 

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

2588 coordinates of all sub-widgets. 

2589 ''' 

2590 

2591 self.do_layout() 

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

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

2594 if descend: 

2595 for child in self.get_children(): 

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

2597 return corners 

2598 

2599 def get_sizes(self): 

2600 

2601 ''' 

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

2603 

2604 Returns a list with size tuples. 

2605 ''' 

2606 self.do_layout() 

2607 sizes = [self.get_size()] 

2608 for child in self.get_children(): 

2609 sizes.extend(child.get_sizes()) 

2610 return sizes 

2611 

2612 def do_layout(self): 

2613 

2614 ''' 

2615 Triggers layouting of the widget hierarchy, if needed. 

2616 ''' 

2617 

2618 if self.parent is not None: 

2619 return self.parent.do_layout() 

2620 

2621 if not self.dirty: 

2622 return 

2623 

2624 sh, sv = self.get_min_size() 

2625 gh, gv = self.get_grow() 

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

2627 sh = 15.*cm 

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

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

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

2631 

2632 def get_children(self): 

2633 

2634 ''' 

2635 Get sub-widgets contained in this widget. 

2636 

2637 Returns a list of widgets. 

2638 ''' 

2639 

2640 return [] 

2641 

2642 def get_size(self): 

2643 

2644 ''' 

2645 Get current size and position of the widget. 

2646 

2647 Triggers layouting and returns 

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

2649 ''' 

2650 

2651 self.do_layout() 

2652 return (self.horizontal.get_value(), 

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

2654 

2655 def get_params(self): 

2656 

2657 ''' 

2658 Get current size and position of the widget. 

2659 

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

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

2662 ''' 

2663 

2664 self.do_layout() 

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

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

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

2668 

2669 def width(self): 

2670 

2671 ''' 

2672 Get current width of the widget. 

2673 

2674 Triggers layouting and returns width. 

2675 ''' 

2676 

2677 self.do_layout() 

2678 return self.horizontal.get_value() 

2679 

2680 def height(self): 

2681 

2682 ''' 

2683 Get current height of the widget. 

2684 

2685 Triggers layouting and return height. 

2686 ''' 

2687 

2688 self.do_layout() 

2689 return self.vertical.get_value() 

2690 

2691 def bbox(self): 

2692 

2693 ''' 

2694 Get PostScript bounding box for this widget. 

2695 

2696 Triggers layouting and returns values suitable to create PS bounding 

2697 box, representing the widgets current size and position. 

2698 ''' 

2699 

2700 self.do_layout() 

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

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

2703 

2704 def dirtyfy(self): 

2705 

2706 ''' 

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

2708 

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

2710 new layouting. 

2711 ''' 

2712 

2713 if self.parent is not None: 

2714 self.parent.dirtyfy() 

2715 

2716 self.dirty = True 

2717 

2718 

2719class CenterLayout(Widget): 

2720 

2721 ''' 

2722 A layout manager which centers its single child widget. 

2723 

2724 The child widget may be oversized. 

2725 ''' 

2726 

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

2728 Widget.__init__(self, horizontal, vertical) 

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

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

2731 

2732 def get_min_size(self): 

2733 shs, svs = Widget.get_min_size(self) 

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

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

2736 

2737 def get_grow(self): 

2738 ghs, gvs = Widget.get_grow(self) 

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

2740 return gh*ghs, gv*gvs 

2741 

2742 def set_size(self, size, offset): 

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

2744 

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

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

2747 if ghc != 0.: 

2748 shc = sh 

2749 if gvc != 0.: 

2750 svc = sv 

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

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

2753 

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

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

2756 

2757 def set_widget(self, widget=None): 

2758 

2759 ''' 

2760 Set the child widget, which shall be centered. 

2761 ''' 

2762 

2763 if widget is None: 

2764 widget = Widget() 

2765 

2766 self.content = widget 

2767 

2768 widget.set_parent(self) 

2769 

2770 def get_widget(self): 

2771 return self.content 

2772 

2773 def get_children(self): 

2774 return [self.content] 

2775 

2776 

2777class FrameLayout(Widget): 

2778 

2779 ''' 

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

2781 widgets. 

2782 

2783 :: 

2784 

2785 +---------------------------+ 

2786 | top | 

2787 +---------------------------+ 

2788 | | | | 

2789 | left | center | right | 

2790 | | | | 

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

2792 | bottom | 

2793 +---------------------------+ 

2794 

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

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

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

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

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

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

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

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

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

2804 spaces between the widgets. 

2805 ''' 

2806 

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

2808 Widget.__init__(self, horizontal, vertical) 

2809 mw = 3.*cm 

2810 self.left = Widget( 

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

2812 self.right = Widget( 

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

2814 self.top = Widget( 

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

2816 parent=self) 

2817 self.bottom = Widget( 

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

2819 parent=self) 

2820 self.center = Widget( 

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

2822 parent=self) 

2823 

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

2825 ''' 

2826 Give margins fixed size constraints. 

2827 ''' 

2828 

2829 self.left.set_horizontal(left, 0) 

2830 self.right.set_horizontal(right, 0) 

2831 self.top.set_vertical(top, 0) 

2832 self.bottom.set_vertical(bottom, 0) 

2833 

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

2835 ''' 

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

2837 

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

2839 ''' 

2840 self.left.set_horizontal(left, grow) 

2841 self.right.set_horizontal(right, grow) 

2842 self.top.set_vertical(top, grow) 

2843 self.bottom.set_vertical(bottom, grow) 

2844 

2845 def get_min_size(self): 

2846 shs, svs = Widget.get_min_size(self) 

2847 

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

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

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

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

2852 

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

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

2855 

2856 # prevent widgets from collapsing 

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

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

2859 shsum += 0.1*cm 

2860 

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

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

2863 svsum += 0.1*cm 

2864 

2865 sh = max(shs, shsum) 

2866 sv = max(svs, svsum) 

2867 

2868 return sh, sv 

2869 

2870 def get_grow(self): 

2871 ghs, gvs = Widget.get_grow(self) 

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

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

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

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

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

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

2878 return gh, gv 

2879 

2880 def set_size(self, size, offset): 

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

2882 

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

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

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

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

2887 

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

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

2890 

2891 if ah < 0.0: 

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

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

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

2895 if av < 0.0: 

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

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

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

2899 

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

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

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

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

2904 

2905 if self.center.aspect is not None: 

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

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

2908 if 0.0 < ahm < ah: 

2909 slh, srh, sch = distribute( 

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

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

2912 

2913 elif 0.0 < avm < av: 

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

2915 sch*self.center.aspect), 

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

2917 

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

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

2920 

2921 oh += ah/2. 

2922 ov += av/2. 

2923 sh -= ah 

2924 sv -= av 

2925 

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

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

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

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

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

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

2932 

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

2934 

2935 ''' 

2936 Set one of the sub-widgets. 

2937 

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

2939 ``'bottom'`` or ``'center'``. 

2940 ''' 

2941 

2942 if widget is None: 

2943 widget = Widget() 

2944 

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

2946 self.__dict__[which] = widget 

2947 else: 

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

2949 

2950 widget.set_parent(self) 

2951 

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

2953 

2954 ''' 

2955 Get one of the sub-widgets. 

2956 

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

2958 ``'bottom'`` or ``'center'``. 

2959 ''' 

2960 

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

2962 return self.__dict__[which] 

2963 else: 

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

2965 

2966 def get_children(self): 

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

2968 

2969 

2970class GridLayout(Widget): 

2971 

2972 ''' 

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

2974 

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

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

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

2978 

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

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

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

2982 might not be resolved optimally. 

2983 ''' 

2984 

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

2986 

2987 ''' 

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

2989 ''' 

2990 

2991 Widget.__init__(self, horizontal, vertical) 

2992 self.grid = [] 

2993 for iy in range(ny): 

2994 row = [] 

2995 for ix in range(nx): 

2996 w = Widget(parent=self) 

2997 row.append(w) 

2998 

2999 self.grid.append(row) 

3000 

3001 def sub_min_sizes_as_array(self): 

3002 esh = num.array( 

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

3004 dtype=float) 

3005 esv = num.array( 

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

3007 dtype=float) 

3008 return esh, esv 

3009 

3010 def sub_grows_as_array(self): 

3011 egh = num.array( 

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

3013 dtype=float) 

3014 egv = num.array( 

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

3016 dtype=float) 

3017 return egh, egv 

3018 

3019 def get_min_size(self): 

3020 sh, sv = Widget.get_min_size(self) 

3021 esh, esv = self.sub_min_sizes_as_array() 

3022 if esh.size != 0: 

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

3024 if esv.size != 0: 

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

3026 return sh, sv 

3027 

3028 def get_grow(self): 

3029 ghs, gvs = Widget.get_grow(self) 

3030 egh, egv = self.sub_grows_as_array() 

3031 if egh.size != 0: 

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

3033 else: 

3034 gh = 1.0 

3035 if egv.size != 0: 

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

3037 else: 

3038 gv = 1.0 

3039 return gh, gv 

3040 

3041 def set_size(self, size, offset): 

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

3043 esh, esv = self.sub_min_sizes_as_array() 

3044 egh, egv = self.sub_grows_as_array() 

3045 

3046 # available additional space 

3047 empty = esh.size == 0 

3048 

3049 if not empty: 

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

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

3052 else: 

3053 av = sv 

3054 ah = sh 

3055 

3056 if ah < 0.0: 

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

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

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

3060 if av < 0.0: 

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

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

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

3064 

3065 nx, ny = esh.shape 

3066 

3067 if not empty: 

3068 # distribute additional space on rows and columns 

3069 # according to grow weights and minimal sizes 

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

3071 nesh = esh.copy() 

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

3073 

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

3075 

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

3077 nesv = esv.copy() 

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

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

3080 

3081 ah = sh - sum(nsh) 

3082 av = sv - sum(nsv) 

3083 

3084 oh += ah/2. 

3085 ov += av/2. 

3086 sh -= ah 

3087 sv -= av 

3088 

3089 # resize child widgets 

3090 neov = ov + sum(nsv) 

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

3092 neov -= nesv 

3093 neoh = oh 

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

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

3096 neoh += nesh 

3097 

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

3099 

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

3101 

3102 ''' 

3103 Set one of the sub-widgets. 

3104 

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

3106 counted from zero. 

3107 ''' 

3108 

3109 if widget is None: 

3110 widget = Widget() 

3111 

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

3113 widget.set_parent(self) 

3114 

3115 def get_widget(self, ix, iy): 

3116 

3117 ''' 

3118 Get one of the sub-widgets. 

3119 

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

3121 counted from zero. 

3122 ''' 

3123 

3124 return self.grid[iy][ix] 

3125 

3126 def get_children(self): 

3127 children = [] 

3128 for row in self.grid: 

3129 children.extend(row) 

3130 

3131 return children 

3132 

3133 

3134def is_gmt5(version='newest'): 

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

3136 

3137 

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

3139 

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

3141 

3142 if gmt.is_gmt5(): 

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

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

3145 gmt.save(fn, crop_eps_mode=True) 

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

3147 s = f.read() 

3148 

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

3150 else: 

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

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

3153 

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

3155 

3156 

3157def text_box( 

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

3159 

3160 gmt = GMT(version=gmtversion) 

3161 if gmt.is_gmt5(): 

3162 row = [0, 0, text] 

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

3164 else: 

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

3166 farg = [] 

3167 

3168 gmt.pstext( 

3169 in_rows=[row], 

3170 finish=True, 

3171 R=(0, 1, 0, 1), 

3172 J='x10p', 

3173 N=True, 

3174 *farg, 

3175 **kwargs) 

3176 

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

3178 gmt.save(fn) 

3179 

3180 (_, stderr) = subprocess.Popen( 

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

3182 stderr=subprocess.PIPE).communicate() 

3183 

3184 dx, dy = None, None 

3185 for line in stderr.splitlines(): 

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

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

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

3189 break 

3190 

3191 return dx, dy 

3192 

3193 

3194class TableLiner(object): 

3195 ''' 

3196 Utility class to turn tables into lines. 

3197 ''' 

3198 

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

3200 self.in_columns = in_columns 

3201 self.in_rows = in_rows 

3202 self.encoding = encoding 

3203 

3204 def __iter__(self): 

3205 if self.in_columns is not None: 

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

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

3208 self.encoding) 

3209 

3210 if self.in_rows is not None: 

3211 for row in self.in_rows: 

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

3213 self.encoding) 

3214 

3215 

3216class LineStreamChopper(object): 

3217 ''' 

3218 File-like object to buffer data. 

3219 ''' 

3220 

3221 def __init__(self, liner): 

3222 self.chopsize = None 

3223 self.liner = liner 

3224 self.chop_iterator = None 

3225 self.closed = False 

3226 

3227 def _chopiter(self): 

3228 buf = BytesIO() 

3229 for line in self.liner: 

3230 buf.write(line) 

3231 buflen = buf.tell() 

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

3233 buf.seek(0) 

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

3235 yield buf.read(self.chopsize) 

3236 

3237 newbuf = BytesIO() 

3238 newbuf.write(buf.read()) 

3239 buf.close() 

3240 buf = newbuf 

3241 

3242 yield(buf.getvalue()) 

3243 buf.close() 

3244 

3245 def read(self, size=None): 

3246 if self.closed: 

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

3248 if self.chop_iterator is None: 

3249 self.chopsize = size 

3250 self.chop_iterator = self._chopiter() 

3251 

3252 self.chopsize = size 

3253 try: 

3254 return next(self.chop_iterator) 

3255 except StopIteration: 

3256 return '' 

3257 

3258 def close(self): 

3259 self.chopsize = None 

3260 self.chop_iterator = None 

3261 self.closed = True 

3262 

3263 def flush(self): 

3264 pass 

3265 

3266 

3267font_tab = { 

3268 0: 'Helvetica', 

3269 1: 'Helvetica-Bold', 

3270} 

3271 

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

3273 

3274 

3275class GMT(object): 

3276 ''' 

3277 A thin wrapper to GMT command execution. 

3278 

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

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

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

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

3283 gmtpy and gmtpy must know where to find it. 

3284 

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

3286 output file. 

3287 

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

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

3290 

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

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

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

3294 

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

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

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

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

3299 

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

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

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

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

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

3305 execution of more than one GMT instance. 

3306 

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

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

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

3310 backward compatibility of the scripts can be maintained. 

3311 

3312 ''' 

3313 

3314 def __init__( 

3315 self, 

3316 config=None, 

3317 kontinue=None, 

3318 version='newest', 

3319 config_papersize=None, 

3320 eps_mode=False): 

3321 

3322 self.installation = get_gmt_installation(version) 

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

3324 self.eps_mode = eps_mode 

3325 self._shutil = shutil 

3326 

3327 if config: 

3328 self.gmt_config.update(config) 

3329 

3330 if config_papersize: 

3331 if not isinstance(config_papersize, str): 

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

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

3334 

3335 if self.is_gmt5(): 

3336 self.gmt_config['PS_MEDIA'] = config_papersize 

3337 else: 

3338 self.gmt_config['PAPER_MEDIA'] = config_papersize 

3339 

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

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

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

3343 

3344 if kontinue is not None: 

3345 self.load_unfinished(kontinue) 

3346 self.needstart = False 

3347 else: 

3348 self.output = BytesIO() 

3349 self.needstart = True 

3350 

3351 self.finished = False 

3352 

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

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

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

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

3357 

3358 self.layout = None 

3359 self.command_log = [] 

3360 self.keep_temp_dir = False 

3361 

3362 def is_gmt5(self): 

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

3364 

3365 def get_version(self): 

3366 return self.installation['version'] 

3367 

3368 def get_config(self, key): 

3369 return self.gmt_config[key] 

3370 

3371 def to_points(self, string): 

3372 if not string: 

3373 return 0 

3374 

3375 unit = string[-1] 

3376 if unit in _units: 

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

3378 else: 

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

3380 return float(string)/_units[default_unit] 

3381 

3382 def label_font_size(self): 

3383 if self.is_gmt5(): 

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

3385 else: 

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

3387 

3388 def label_font(self): 

3389 if self.is_gmt5(): 

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

3391 else: 

3392 return self.gmt_config['LABEL_FONT'] 

3393 

3394 def gen_gmt_config_file(self, config_filename, config): 

3395 f = open(config_filename, 'wb') 

3396 f.write( 

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

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

3399 

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

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

3402 f.close() 

3403 

3404 def __del__(self): 

3405 if not self.keep_temp_dir: 

3406 self._shutil.rmtree(self.tempdir) 

3407 

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

3409 

3410 ''' 

3411 Execute arbitrary GMT command. 

3412 

3413 See docstring in __getattr__ for details. 

3414 ''' 

3415 

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

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

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

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

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

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

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

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

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

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

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

3427 

3428 assert(not self.finished) 

3429 

3430 # check for mutual exclusiveness on input and output possibilities 

3431 assert(1 >= len( 

3432 [x for x in [ 

3433 in_stream, in_filename, in_string, in_columns, in_rows] 

3434 if x is not None])) 

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

3436 if x is not None])) 

3437 

3438 options = [] 

3439 

3440 gmt_config = self.gmt_config 

3441 if not self.is_gmt5(): 

3442 gmt_config_filename = self.gmt_config_filename 

3443 if config_override: 

3444 gmt_config = self.gmt_config.copy() 

3445 gmt_config.update(config_override) 

3446 gmt_config_override_filename = pjoin( 

3447 self.tempdir, 'gmtdefaults_override') 

3448 self.gen_gmt_config_file( 

3449 gmt_config_override_filename, gmt_config) 

3450 gmt_config_filename = gmt_config_override_filename 

3451 

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

3453 if config_override: 

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

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

3456 

3457 if out_discard: 

3458 out_filename = '/dev/null' 

3459 

3460 out_mustclose = False 

3461 if out_filename is not None: 

3462 out_mustclose = True 

3463 out_stream = open(out_filename, 'wb') 

3464 

3465 if in_filename is not None: 

3466 in_stream = open(in_filename, 'rb') 

3467 

3468 if in_string is not None: 

3469 in_stream = BytesIO(in_string) 

3470 

3471 encoding_gmt = gmt_config.get( 

3472 'PS_CHAR_ENCODING', 

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

3474 

3475 encoding = encoding_gmt_to_python[encoding_gmt.lower()] 

3476 

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

3478 in_stream = LineStreamChopper(TableLiner(in_columns=in_columns, 

3479 in_rows=in_rows, 

3480 encoding=encoding)) 

3481 

3482 # convert option arguments to strings 

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

3484 if len(k) > 1: 

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

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

3487 % (k, command)) 

3488 

3489 if type(v) is bool: 

3490 if v: 

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

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

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

3494 else: 

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

3496 

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

3498 if out_stream is None: 

3499 if not finish: 

3500 options.append('-K') 

3501 else: 

3502 self.finished = True 

3503 

3504 if not self.needstart: 

3505 options.append('-O') 

3506 else: 

3507 self.needstart = False 

3508 

3509 out_stream = self.output 

3510 

3511 # run the command 

3512 if self.is_gmt5(): 

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

3514 else: 

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

3516 

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

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

3519 args.extend(options) 

3520 args.extend(addargs) 

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

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

3523 args.append('+'+gmt_config_filename) 

3524 

3525 bs = 2048 

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

3527 stdout=subprocess.PIPE, bufsize=bs, 

3528 env=self.environ) 

3529 while True: 

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

3531 if cr: 

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

3533 if cw: 

3534 if in_stream is not None: 

3535 data = in_stream.read(bs) 

3536 if len(data) == 0: 

3537 break 

3538 p.stdin.write(data) 

3539 else: 

3540 break 

3541 if not cr and not cw: 

3542 break 

3543 

3544 p.stdin.close() 

3545 

3546 while True: 

3547 data = p.stdout.read(bs) 

3548 if len(data) == 0: 

3549 break 

3550 out_stream.write(data) 

3551 

3552 p.stdout.close() 

3553 

3554 retcode = p.wait() 

3555 

3556 if in_stream is not None: 

3557 in_stream.close() 

3558 

3559 if out_mustclose: 

3560 out_stream.close() 

3561 

3562 if retcode != 0: 

3563 self.keep_temp_dir = True 

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

3565 'While executing command:\n%s' 

3566 % (command, escape_shell_args(args))) 

3567 

3568 self.command_log.append(args) 

3569 

3570 def __getattr__(self, command): 

3571 

3572 ''' 

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

3574 

3575 Execute arbitrary GMT command. 

3576 

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

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

3579 called. 

3580 

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

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

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

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

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

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

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

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

3589 

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

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

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

3593 not interested in the output. 

3594 

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

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

3597 

3598 =============== ======================================================= 

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

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

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

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

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

3604 ascii 

3605 table, which is fed to the process. 

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

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

3608 table, which is fed to the process. 

3609 =============== ======================================================= 

3610 

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

3612 following options: 

3613 

3614 ================= ===================================================== 

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

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

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

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

3619 

3620 Additional keyword arguments: 

3621 

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

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

3624 currently active set of defaults exclusively 

3625 during this call. 

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

3627 by the GMT instance is finished, and no further 

3628 plotting is allowed. 

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

3630 option to the command. 

3631 ===================== ================================================= 

3632 

3633 ''' 

3634 

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

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

3637 return f 

3638 

3639 def tempfilename(self, name=None): 

3640 ''' 

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

3642 

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

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

3645 ''' 

3646 

3647 if not name: 

3648 name = ''.join( 

3649 [random.choice('abcdefghijklmnopqrstuvwxyz') 

3650 for i in range(10)]) 

3651 

3652 fn = pjoin(self.tempdir, name) 

3653 return fn 

3654 

3655 def tempfile(self, name=None): 

3656 ''' 

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

3658 ''' 

3659 

3660 fn = self.tempfilename(name) 

3661 f = open(fn, 'wb') 

3662 return f, fn 

3663 

3664 def save_unfinished(self, filename): 

3665 out = open(filename, 'wb') 

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

3667 out.close() 

3668 

3669 def load_unfinished(self, filename): 

3670 self.output = BytesIO() 

3671 self.finished = False 

3672 inp = open(filename, 'rb') 

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

3674 inp.close() 

3675 

3676 def dump(self, ident): 

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

3678 self.save_unfinished(filename) 

3679 

3680 def load(self, ident): 

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

3682 self.load_unfinished(filename) 

3683 

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

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

3686 psconvert=False): 

3687 

3688 ''' 

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

3690 

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

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

3693 

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

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

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

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

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

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

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

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

3702 

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

3704 ''' 

3705 

3706 if not self.finished: 

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

3708 

3709 if filename: 

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

3711 out = open(tempfn, 'wb') 

3712 else: 

3713 out = sys.stdout 

3714 

3715 if bbox and not self.is_gmt5(): 

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

3717 else: 

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

3719 

3720 if filename: 

3721 out.close() 

3722 

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

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

3725 

3726 shutil.move(tempfn, filename) 

3727 return 

3728 

3729 if self.is_gmt5(): 

3730 if crop_eps_mode: 

3731 addarg = ['-A'] 

3732 else: 

3733 addarg = [] 

3734 

3735 subprocess.call( 

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

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

3738 

3739 if bbox: 

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

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

3742 replace_bbox(bbox, fin, fout) 

3743 

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

3745 

3746 else: 

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

3748 

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

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

3751 return 

3752 

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

3754 if psconvert: 

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

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

3757 '-F' + filename]) 

3758 else: 

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

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

3761 else: 

3762 subprocess.call([ 

3763 'gmtpy-epstopdf', 

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

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

3766 

3767 convert_graph( 

3768 tempfn + '.pdf', filename, 

3769 resolution=resolution, oversample=oversample, 

3770 size=size, width=width, height=height) 

3771 

3772 def bbox(self): 

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

3774 

3775 def get_command_log(self): 

3776 ''' 

3777 Get the command log. 

3778 ''' 

3779 

3780 return self.command_log 

3781 

3782 def __str__(self): 

3783 s = '' 

3784 for com in self.command_log: 

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

3786 return s 

3787 

3788 def page_size_points(self): 

3789 ''' 

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

3791 ''' 

3792 

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

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

3795 pm = pm[:-1] 

3796 

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

3798 

3799 if pm in all_paper_sizes(): 

3800 

3801 if orient == 'portrait': 

3802 return get_paper_size(pm) 

3803 else: 

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

3805 

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

3807 if m: 

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

3809 w, h = float(w), float(h) 

3810 if uw: 

3811 w *= _units[uw] 

3812 if uh: 

3813 h *= _units[uh] 

3814 if orient == 'portrait': 

3815 return w, h 

3816 else: 

3817 return h, w 

3818 

3819 return None, None 

3820 

3821 def default_layout(self, with_palette=False): 

3822 ''' 

3823 Get a default layout for the output page. 

3824 

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

3826 `PAPER_MEDIA` setting in the GMT configuration dict. 

3827 

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

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

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

3831 :py:class:`FrameLayout`. 

3832 

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

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

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

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

3837 

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

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

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

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

3842 is preserved. 

3843 

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

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

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

3847 ''' 

3848 

3849 if self.layout is None: 

3850 w, h = self.page_size_points() 

3851 

3852 if w is None or h is None: 

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

3854 

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

3856 

3857 if with_palette: 

3858 palette_layout = GridLayout(3, 1) 

3859 spacer = palette_layout.get_widget(1, 0) 

3860 palette_widget = palette_layout.get_widget(2, 0) 

3861 spacer.set_horizontal(0.5*cm) 

3862 palette_widget.set_horizontal(0.5*cm) 

3863 

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

3865 outer = CenterLayout() 

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

3867 inner = FrameLayout() 

3868 outer.set_widget(inner) 

3869 if with_palette: 

3870 inner.set_widget('center', palette_layout) 

3871 widget = palette_layout 

3872 else: 

3873 widget = inner.get_widget('center') 

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

3875 aspect=1./golden_ratio) 

3876 mw = 3.0*cm 

3877 inner.set_fixed_margins( 

3878 mw, mw, mw/golden_ratio, mw/golden_ratio) 

3879 self.layout = inner 

3880 

3881 elif pm.startswith('custom_'): 

3882 layout = FrameLayout() 

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

3884 mw = 3.0*cm 

3885 layout.set_min_margins( 

3886 mw, mw, mw/golden_ratio, mw/golden_ratio) 

3887 if with_palette: 

3888 layout.set_widget('center', palette_layout) 

3889 self.layout = layout 

3890 else: 

3891 outer = FrameLayout() 

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

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

3894 

3895 inner = FrameLayout() 

3896 outer.set_widget('center', inner) 

3897 mw = 3.0*cm 

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

3899 if with_palette: 

3900 inner.set_widget('center', palette_layout) 

3901 widget = palette_layout 

3902 else: 

3903 widget = inner.get_widget('center') 

3904 

3905 widget.set_aspect(1./golden_ratio) 

3906 

3907 self.layout = inner 

3908 

3909 return self.layout 

3910 

3911 def draw_layout(self, layout): 

3912 ''' 

3913 Use psxy to draw layout; for debugging 

3914 ''' 

3915 

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

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

3918 rects_wid = rects[:, 0, 0] 

3919 rects_hei = rects[:, 0, 1] 

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

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

3922 nrects = len(rects) 

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

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

3925 

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

3927 

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

3929 self.makecpt( 

3930 C='ocean', 

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

3932 Z=True, 

3933 out_filename=cptfile, suppress_defaults=True) 

3934 

3935 bb = layout.bbox() 

3936 self.psxy( 

3937 in_columns=prects, 

3938 C=cptfile, 

3939 W='1p', 

3940 S='J', 

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

3942 *layout.XYJ()) 

3943 

3944 

3945def simpleconf_to_ax(conf, axname): 

3946 c = {} 

3947 x = axname 

3948 for x in ('', axname): 

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

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

3951 'snap'): 

3952 

3953 if x+k in conf: 

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

3955 

3956 return Ax(**c) 

3957 

3958 

3959class DensityPlotDef(object): 

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

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

3962 self.data = data 

3963 self.cpt = cpt 

3964 self.tension = tension 

3965 self.size = size 

3966 self.contour = contour 

3967 self.method = method 

3968 self.zscaler = zscaler 

3969 self.extra = extra 

3970 

3971 

3972class TextDef(object): 

3973 def __init__( 

3974 self, 

3975 data, 

3976 size=9, 

3977 justify='MC', 

3978 fontno=0, 

3979 offset=(0, 0), 

3980 color='black'): 

3981 

3982 self.data = data 

3983 self.size = size 

3984 self.justify = justify 

3985 self.fontno = fontno 

3986 self.offset = offset 

3987 self.color = color 

3988 

3989 

3990class Simple(object): 

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

3992 self.data = [] 

3993 self.symbols = [] 

3994 self.config = copy.deepcopy(simple_config) 

3995 self.gmtconfig = gmtconfig 

3996 self.density_plot_defs = [] 

3997 self.text_defs = [] 

3998 

3999 self.gmtversion = gmtversion 

4000 

4001 self.data_x = [] 

4002 self.symbols_x = [] 

4003 

4004 self.data_y = [] 

4005 self.symbols_y = [] 

4006 

4007 self.default_config = {} 

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

4009 height=15.*cm / golden_ratio, 

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

4011 with_palette=False, 

4012 palette_offset=0.5*cm, 

4013 palette_width=None, 

4014 palette_height=None, 

4015 zlabeloffset=2*cm, 

4016 draw_layout=False) 

4017 

4018 self.setup_defaults() 

4019 self.fixate_widget_aspect = False 

4020 

4021 def setup_defaults(self): 

4022 pass 

4023 

4024 def set_defaults(self, **kwargs): 

4025 self.default_config.update(kwargs) 

4026 

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

4028 self.data.append(data) 

4029 self.symbols.append(symbol) 

4030 

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

4032 dpd = DensityPlotDef(data, **kwargs) 

4033 self.density_plot_defs.append(dpd) 

4034 

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

4036 dpd = TextDef(data, **kwargs) 

4037 self.text_defs.append(dpd) 

4038 

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

4040 self.data_x.append(data) 

4041 self.symbols_x.append(symbol) 

4042 

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

4044 self.data_y.append(data) 

4045 self.symbols_y.append(symbol) 

4046 

4047 def set(self, **kwargs): 

4048 self.config.update(kwargs) 

4049 

4050 def setup_base(self, conf): 

4051 w = conf.pop('width') 

4052 h = conf.pop('height') 

4053 margins = conf.pop('margins') 

4054 

4055 gmtconfig = {} 

4056 if self.gmtconfig is not None: 

4057 gmtconfig.update(self.gmtconfig) 

4058 

4059 gmt = GMT( 

4060 version=self.gmtversion, 

4061 config=gmtconfig, 

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

4063 

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

4065 layout.set_min_margins(*margins) 

4066 if conf['with_palette']: 

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

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

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

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

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

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

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

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

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

4076 return gmt, layout, widget, palette_widget 

4077 else: 

4078 widget = layout.get_widget() 

4079 return gmt, layout, widget, None 

4080 

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

4082 pass 

4083 

4084 def setup_scaling(self, conf): 

4085 ndims = 2 

4086 if self.density_plot_defs: 

4087 ndims = 3 

4088 

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

4090 

4091 data_all = [] 

4092 data_all.extend(self.data) 

4093 for dsd in self.density_plot_defs: 

4094 if dsd.zscaler is None: 

4095 data_all.append(dsd.data) 

4096 else: 

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

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

4099 

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

4101 

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

4103 

4104 return scaler 

4105 

4106 def setup_scaling_plus(self, scaler, axes): 

4107 pass 

4108 

4109 def setup_scaling_extra(self, scaler, conf): 

4110 

4111 scaler_x = scaler.copy() 

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

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

4114 

4115 scaler_y = scaler.copy() 

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

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

4118 

4119 return scaler_x, scaler_y 

4120 

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

4122 

4123 R = scaler.R() 

4124 # par = scaler.get_params() 

4125 rxyj = R + widget.XYJ() 

4126 innerticks = False 

4127 for dpd in self.density_plot_defs: 

4128 

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

4130 

4131 if dpd.zscaler is not None: 

4132 s = dpd.zscaler 

4133 else: 

4134 s = scaler 

4135 

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

4137 

4138 fn_grid = gmt.tempfilename() 

4139 

4140 fn_mean = gmt.tempfilename() 

4141 

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

4143 gmt.blockmean(in_columns=dpd.data, 

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

4145 out_filename=fn_mean, *R) 

4146 

4147 if dpd.method == 'surface': 

4148 gmt.surface( 

4149 in_filename=fn_mean, 

4150 T=dpd.tension, 

4151 G=fn_grid, 

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

4153 out_discard=True, 

4154 *R) 

4155 

4156 if dpd.method == 'triangulate': 

4157 gmt.triangulate( 

4158 in_filename=fn_mean, 

4159 G=fn_grid, 

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

4161 out_discard=True, 

4162 V=True, 

4163 *R) 

4164 

4165 if gmt.is_gmt5(): 

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

4167 

4168 else: 

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

4170 

4171 if dpd.contour: 

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

4173 innerticks = '0.5p,black' 

4174 

4175 os.remove(fn_grid) 

4176 os.remove(fn_mean) 

4177 

4178 if dpd.method == 'fillcontour': 

4179 extra = dict(C=fn_cpt) 

4180 extra.update(dpd.extra) 

4181 gmt.pscontour(in_columns=dpd.data, 

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

4183 

4184 if dpd.method == 'contour': 

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

4186 extra.update(dpd.extra) 

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

4188 

4189 return fn_cpt, innerticks 

4190 

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

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

4193 

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

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

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

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

4198 

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

4200 pass 

4201 

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

4203 pass 

4204 

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

4206 

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

4208 gmt.psxy(in_columns=dat, 

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

4210 

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

4212 gmt.psxy(in_columns=dat, 

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

4214 

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

4216 

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

4218 for td in self.text_defs: 

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

4220 text = td.data[-1] 

4221 size = td.size 

4222 angle = 0 

4223 fontno = td.fontno 

4224 justify = td.justify 

4225 color = td.color 

4226 if gmt.is_gmt5(): 

4227 gmt.pstext( 

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

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

4230 size, fontno, color, angle, justify), 

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

4232 else: 

4233 gmt.pstext( 

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

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

4236 

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

4238 

4239 conf = dict(self.default_config) 

4240 conf.update(self.config) 

4241 

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

4243 scaler = self.setup_scaling(conf) 

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

4245 

4246 self.setup_projection(widget, scaler, conf) 

4247 if self.fixate_widget_aspect: 

4248 aspect = aspect_for_projection( 

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

4250 

4251 widget.set_aspect(aspect) 

4252 

4253 if conf['draw_layout']: 

4254 gmt.draw_layout(layout) 

4255 cptfile = None 

4256 if self.density_plot_defs: 

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

4258 self.pre_draw(gmt, widget, scaler) 

4259 self.draw(gmt, widget, scaler) 

4260 self.post_draw(gmt, widget, scaler) 

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

4262 self.draw_text(gmt, widget, scaler) 

4263 self.draw_basemap(gmt, widget, scaler) 

4264 

4265 if palette_widget and cptfile: 

4266 nice_palette(gmt, palette_widget, scaler, cptfile, 

4267 innerticks=innerticks, 

4268 zlabeloffset=conf['zlabeloffset']) 

4269 

4270 gmt.save(filename, resolution=resolution) 

4271 

4272 

4273class LinLinPlot(Simple): 

4274 pass 

4275 

4276 

4277class LogLinPlot(Simple): 

4278 

4279 def setup_defaults(self): 

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

4281 

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

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

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

4285 

4286 

4287class LinLogPlot(Simple): 

4288 

4289 def setup_defaults(self): 

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

4291 

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

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

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

4295 

4296 

4297class LogLogPlot(Simple): 

4298 

4299 def setup_defaults(self): 

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

4301 

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

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

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

4305 

4306 

4307class AziDistPlot(Simple): 

4308 

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

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

4311 self.fixate_widget_aspect = True 

4312 

4313 def setup_defaults(self): 

4314 self.set_defaults( 

4315 height=15.*cm, 

4316 width=15.*cm, 

4317 xmode='off', 

4318 xlimits=(0., 360.), 

4319 xinc=45.) 

4320 

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

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

4323 

4324 def setup_scaling_plus(self, scaler, axes): 

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

4326 

4327 

4328class MPlot(Simple): 

4329 

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

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

4332 self.fixate_widget_aspect = True 

4333 

4334 def setup_defaults(self): 

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

4336 

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

4338 par = scaler.get_params() 

4339 lon0 = (par['xmin'] + par['xmax'])/2. 

4340 lat0 = (par['ymin'] + par['ymax'])/2. 

4341 sll = '%g/%g' % (lon0, lat0) 

4342 widget['J'] = '-JM' + sll + '/%(width)gp' 

4343 scaler['B'] = \ 

4344 '-B%(xinc)gg%(xinc)g:%(xlabel)s:/%(yinc)gg%(yinc)g:%(ylabel)s:WSen' 

4345 

4346 

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

4348 innerticks=True): 

4349 

4350 par = scaleguru.get_params() 

4351 par_ax = scaleguru.get_params(ax_projection=True) 

4352 nz_palette = int(widget.height()/inch * 300) 

4353 px = num.zeros(nz_palette*2) 

4354 px[1::2] += 1 

4355 pz = num.linspace(par['zmin'], par['zmax'], nz_palette).repeat(2) 

4356 pdz = pz[2]-pz[0] 

4357 palgrdfile = gmt.tempfilename() 

4358 pal_r = (0, 1, par['zmin'], par['zmax']) 

4359 pal_ax_r = (0, 1, par_ax['zmin'], par_ax['zmax']) 

4360 gmt.xyz2grd( 

4361 G=palgrdfile, R=pal_r, 

4362 I=(1, pdz), in_columns=(px, pz, pz), # noqa 

4363 out_discard=True) 

4364 

4365 gmt.grdimage(palgrdfile, R=pal_r, C=cptfile, *widget.JXY()) 

4366 if isinstance(innerticks, str): 

4367 tickpen = innerticks 

4368 gmt.grdcontour(palgrdfile, W=tickpen, R=pal_r, C=cptfile, 

4369 *widget.JXY()) 

4370 

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

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

4373 ticklen = negpalwid 

4374 else: 

4375 ticklen = '0p' 

4376 

4377 TICK_LENGTH_PARAM = 'MAP_TICK_LENGTH' if gmt.is_gmt5() else 'TICK_LENGTH' 

4378 gmt.psbasemap( 

4379 R=pal_ax_r, B='4::/%(zinc)g::nsw' % par_ax, 

4380 config={TICK_LENGTH_PARAM: ticklen}, 

4381 *widget.JXY()) 

4382 

4383 if innerticks: 

4384 gmt.psbasemap( 

4385 R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax, 

4386 config={TICK_LENGTH_PARAM: '0p'}, 

4387 *widget.JXY()) 

4388 else: 

4389 gmt.psbasemap(R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax, *widget.JXY()) 

4390 

4391 if par_ax['zlabel']: 

4392 label_font = gmt.label_font() 

4393 label_font_size = gmt.label_font_size() 

4394 label_offset = zlabeloffset 

4395 gmt.pstext( 

4396 R=(0, 1, 0, 2), D="%gp/0p" % label_offset, 

4397 N=True, 

4398 in_rows=[(1, 1, label_font_size, -90, label_font, 'CB', 

4399 par_ax['zlabel'])], 

4400 *widget.JXY())