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 if version[0] == '6': 

1301 raise GMTInstallationProblem( 

1302 'pyrocko.gmtpy does not support GMT 6') 

1303 

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

1305 gmtdefaults = pjoin(bin_dir, 'gmtdefaults') 

1306 

1307 versionfound = get_gmt_version(gmtdefaults, home_dir) 

1308 

1309 if versionfound != version: 

1310 raise GMTInstallationProblem(( 

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

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

1313 version, versionfound, gmtdefaults)) 

1314 

1315 

1316def get_gmt_installation(version): 

1317 setup_gmt_installations() 

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

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

1320 % (version, newest_installed_gmt_version())) 

1321 

1322 version = 'newest' 

1323 

1324 if version == 'newest': 

1325 version = newest_installed_gmt_version() 

1326 

1327 installation = dict(_gmt_installations[version]) 

1328 

1329 return installation 

1330 

1331 

1332def setup_gmt_installations(): 

1333 if not setup_gmt_installations.have_done: 

1334 if not _gmt_installations: 

1335 

1336 _gmt_installations.update(detect_gmt_installations()) 

1337 

1338 # store defaults as dicts into the gmt installations dicts 

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

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

1341 installation['version'] = version 

1342 

1343 for installation in _gmt_installations.values(): 

1344 check_gmt_installation(installation) 

1345 

1346 setup_gmt_installations.have_done = True 

1347 

1348 

1349setup_gmt_installations.have_done = False 

1350 

1351_paper_sizes_a = '''A0 2380 3368 

1352 A1 1684 2380 

1353 A2 1190 1684 

1354 A3 842 1190 

1355 A4 595 842 

1356 A5 421 595 

1357 A6 297 421 

1358 A7 210 297 

1359 A8 148 210 

1360 A9 105 148 

1361 A10 74 105 

1362 B0 2836 4008 

1363 B1 2004 2836 

1364 B2 1418 2004 

1365 B3 1002 1418 

1366 B4 709 1002 

1367 B5 501 709 

1368 archA 648 864 

1369 archB 864 1296 

1370 archC 1296 1728 

1371 archD 1728 2592 

1372 archE 2592 3456 

1373 flsa 612 936 

1374 halfletter 396 612 

1375 note 540 720 

1376 letter 612 792 

1377 legal 612 1008 

1378 11x17 792 1224 

1379 ledger 1224 792''' 

1380 

1381 

1382_paper_sizes = {} 

1383 

1384 

1385def setup_paper_sizes(): 

1386 if not _paper_sizes: 

1387 for line in _paper_sizes_a.splitlines(): 

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

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

1390 

1391 

1392def get_paper_size(k): 

1393 setup_paper_sizes() 

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

1395 

1396 

1397def all_paper_sizes(): 

1398 setup_paper_sizes() 

1399 return _paper_sizes 

1400 

1401 

1402def measure_unit(gmt_config): 

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

1404 if k in gmt_config: 

1405 return gmt_config[k] 

1406 

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

1408 

1409 

1410def paper_media(gmt_config): 

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

1412 if k in gmt_config: 

1413 return gmt_config[k] 

1414 

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

1416 

1417 

1418def page_orientation(gmt_config): 

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

1420 if k in gmt_config: 

1421 return gmt_config[k] 

1422 

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

1424 

1425 

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

1427 

1428 leftmargin, topmargin, rightmargin, bottommargin = margins 

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

1430 

1431 paper_size = get_paper_size(paper_media(gmt_config)) 

1432 if not portrait: 

1433 paper_size = paper_size[1], paper_size[0] 

1434 

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

1436 2.0 + leftmargin 

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

1438 2.0 + bottommargin 

1439 

1440 if portrait: 

1441 bb1 = int((xoffset - leftmargin)) 

1442 bb2 = int((yoffset - bottommargin)) 

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

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

1445 else: 

1446 bb1 = int((yoffset - topmargin)) 

1447 bb2 = int((xoffset - leftmargin)) 

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

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

1450 

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

1452 

1453 

1454def gmtdefaults_as_text(version='newest'): 

1455 

1456 ''' 

1457 Get the built-in gmtdefaults. 

1458 ''' 

1459 

1460 if version not in _gmt_installations: 

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

1462 % (version, newest_installed_gmt_version())) 

1463 version = 'newest' 

1464 

1465 if version == 'newest': 

1466 version = newest_installed_gmt_version() 

1467 

1468 return _gmt_defaults_by_version[version] 

1469 

1470 

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

1472 ''' 

1473 Write COARDS compliant netcdf (grd) file. 

1474 ''' 

1475 

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

1477 ny, nx = z.shape 

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

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

1480 

1481 if naming == 'xy': 

1482 kx, ky = 'x', 'y' 

1483 else: 

1484 kx, ky = 'lon', 'lat' 

1485 

1486 nc.node_offset = 0 

1487 if title is not None: 

1488 nc.title = title 

1489 

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

1491 nc.createDimension(kx, nx) 

1492 nc.createDimension(ky, ny) 

1493 

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

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

1496 if naming == 'xy': 

1497 xvar.long_name = kx 

1498 yvar.long_name = ky 

1499 else: 

1500 xvar.long_name = 'longitude' 

1501 xvar.units = 'degrees_east' 

1502 yvar.long_name = 'latitude' 

1503 yvar.units = 'degrees_north' 

1504 

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

1506 

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

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

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

1510 

1511 nc.close() 

1512 

1513 

1514def to_array(var): 

1515 arr = var[:].copy() 

1516 if hasattr(var, 'scale_factor'): 

1517 arr *= var.scale_factor 

1518 

1519 if hasattr(var, 'add_offset'): 

1520 arr += var.add_offset 

1521 

1522 return arr 

1523 

1524 

1525def loadgrd(filename): 

1526 ''' 

1527 Read COARDS compliant netcdf (grd) file. 

1528 ''' 

1529 

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

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

1532 kx = 'x' 

1533 ky = 'y' 

1534 if 'lon' in vkeys: 

1535 kx = 'lon' 

1536 if 'lat' in vkeys: 

1537 ky = 'lat' 

1538 

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

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

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

1542 

1543 nc.close() 

1544 return x, y, z 

1545 

1546 

1547def centers_to_edges(asorted): 

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

1549 

1550 

1551def nvals(asorted): 

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

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

1554 

1555 

1556def guess_vals(asorted): 

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

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

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

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

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

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

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

1564 

1565 

1566def blockmean(asorted, b): 

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

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

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

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

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

1572 return ( 

1573 asorted[indis[:-1]], 

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

1575 

1576 

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

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

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

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

1581 

1582 zindi = yindi*nx+xindi 

1583 order = num.argsort(zindi) 

1584 z = z[order] 

1585 zindi = zindi[order] 

1586 

1587 zindi, z = blockmean(zindi, z) 

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

1589 znew[:] = num.nan 

1590 znew[zindi] = z 

1591 return znew.reshape(ny, nx) 

1592 

1593 

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

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

1596 xs = x_sorted 

1597 ys = y_sorted 

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

1599 if mode == 'nonrandom': 

1600 return nxs, nys, 0 

1601 elif xs.size == nxs*nys: 

1602 # exact match 

1603 return nxs, nys, 0 

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

1605 # possibly randomly sampled 

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

1607 nys = nxs 

1608 return nxs, nys, 2 

1609 else: 

1610 return nxs, nys, 1 

1611 

1612 

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

1614 ''' 

1615 Grid tabular XYZ data by binning. 

1616 

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

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

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

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

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

1622 

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

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

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

1626 ''' 

1627 

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

1629 assert x.size == y.size == z.size 

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

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

1632 if badness <= 1: 

1633 xf = guess_vals(xs) 

1634 yf = guess_vals(ys) 

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

1636 else: 

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

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

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

1640 

1641 return xf, yf, zf 

1642 

1643 

1644def tabledata(xf, yf, zf): 

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

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

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

1648 z = zf.flatten() 

1649 return x, y, z 

1650 

1651 

1652def double1d(a): 

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

1654 a2[::2] = a 

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

1656 return a2 

1657 

1658 

1659def double2d(f): 

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

1661 f2[:, :] = num.nan 

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

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

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

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

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

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

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

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

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

1671 return f2 

1672 

1673 

1674def doublegrid(x, y, z): 

1675 x2 = double1d(x) 

1676 y2 = double1d(y) 

1677 z2 = double2d(z) 

1678 return x2, y2, z2 

1679 

1680 

1681class Guru(object): 

1682 ''' 

1683 Abstract base class providing template interpolation, accessible as 

1684 attributes. 

1685 

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

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

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

1689 with the templates. 

1690 ''' 

1691 

1692 def __init__(self): 

1693 self.templates = {} 

1694 

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

1696 params = self.get_params(**kwargs) 

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

1698 return strings 

1699 

1700 # hand through templates dict 

1701 def __getitem__(self, template_name): 

1702 return self.templates[template_name] 

1703 

1704 def __setitem__(self, template_name, template): 

1705 self.templates[template_name] = template 

1706 

1707 def __contains__(self, template_name): 

1708 return template_name in self.templates 

1709 

1710 def __iter__(self): 

1711 return iter(self.templates) 

1712 

1713 def __len__(self): 

1714 return len(self.templates) 

1715 

1716 def __delitem__(self, template_name): 

1717 del(self.templates[template_name]) 

1718 

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

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

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

1722 

1723 def __getattr__(self, template_names): 

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

1725 raise AttributeError(template_names) 

1726 

1727 def f(**kwargs): 

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

1729 

1730 return f 

1731 

1732 

1733def nice_value(x): 

1734 ''' 

1735 Round ``x`` to nice value. 

1736 ''' 

1737 

1738 exp = 1.0 

1739 sign = 1 

1740 if x < 0.0: 

1741 x = -x 

1742 sign = -1 

1743 while x >= 1.0: 

1744 x /= 10.0 

1745 exp *= 10.0 

1746 while x < 0.1: 

1747 x *= 10.0 

1748 exp /= 10.0 

1749 

1750 if x >= 0.75: 

1751 return sign * 1.0 * exp 

1752 if x >= 0.375: 

1753 return sign * 0.5 * exp 

1754 if x >= 0.225: 

1755 return sign * 0.25 * exp 

1756 if x >= 0.15: 

1757 return sign * 0.2 * exp 

1758 

1759 return sign * 0.1 * exp 

1760 

1761 

1762class AutoScaler(object): 

1763 ''' 

1764 Tunable 1D autoscaling based on data range. 

1765 

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

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

1768 notation. 

1769 

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

1771 

1772 .. py:attribute:: approx_ticks 

1773 

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

1775 

1776 .. py:attribute:: mode 

1777 

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

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

1780 

1781 ================ ================================================== 

1782 mode description 

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

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

1785 below. 

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

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

1788 max. 

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

1790 zero. 

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

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

1793 disabled, such that the output range always 

1794 exactly matches the data range. 

1795 ================ ================================================== 

1796 

1797 .. py:attribute:: exp 

1798 

1799 If defined, override automatically determined exponent for notation 

1800 by the given value. 

1801 

1802 .. py:attribute:: snap 

1803 

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

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

1806 

1807 .. py:attribute:: inc 

1808 

1809 If defined, override automatically determined tick increment by the 

1810 given value. 

1811 

1812 .. py:attribute:: space 

1813 

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

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

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

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

1818 

1819 .. py:attribute:: exp_factor 

1820 

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

1822 

1823 .. py:attribute:: no_exp_interval: 

1824 

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

1826 

1827 ''' 

1828 

1829 def __init__( 

1830 self, 

1831 approx_ticks=7.0, 

1832 mode='auto', 

1833 exp=None, 

1834 snap=False, 

1835 inc=None, 

1836 space=0.0, 

1837 exp_factor=3, 

1838 no_exp_interval=(-3, 5)): 

1839 

1840 ''' 

1841 Create new AutoScaler instance. 

1842 

1843 The parameters are described in the AutoScaler documentation. 

1844 ''' 

1845 

1846 self.approx_ticks = approx_ticks 

1847 self.mode = mode 

1848 self.exp = exp 

1849 self.snap = snap 

1850 self.inc = inc 

1851 self.space = space 

1852 self.exp_factor = exp_factor 

1853 self.no_exp_interval = no_exp_interval 

1854 

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

1856 

1857 ''' 

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

1859 

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

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

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

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

1864 value. 

1865 ''' 

1866 

1867 data_min = min(data_range) 

1868 data_max = max(data_range) 

1869 

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

1871 

1872 a = self.mode 

1873 if self.mode == 'auto': 

1874 a = self.guess_autoscale_mode(data_min, data_max) 

1875 

1876 if override_mode is not None: 

1877 a = override_mode 

1878 

1879 mi, ma = 0, 0 

1880 if a == 'off': 

1881 mi, ma = data_min, data_max 

1882 elif a == '0-max': 

1883 mi = 0.0 

1884 if data_max > 0.0: 

1885 ma = data_max 

1886 else: 

1887 ma = 1.0 

1888 elif a == 'min-0': 

1889 ma = 0.0 

1890 if data_min < 0.0: 

1891 mi = data_min 

1892 else: 

1893 mi = -1.0 

1894 elif a == 'min-max': 

1895 mi, ma = data_min, data_max 

1896 elif a == 'symmetric': 

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

1898 mi = -m 

1899 ma = m 

1900 

1901 nmi = mi 

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

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

1904 

1905 nma = ma 

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

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

1908 

1909 mi, ma = nmi, nma 

1910 

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

1912 mi -= 1.0 

1913 ma += 1.0 

1914 

1915 # make nice tick increment 

1916 if self.inc is not None: 

1917 inc = self.inc 

1918 else: 

1919 if self.approx_ticks > 0.: 

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

1921 else: 

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

1923 

1924 if inc == 0.0: 

1925 inc = 1.0 

1926 

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

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

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

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

1931 

1932 if is_reverse: 

1933 return ma, mi, -inc 

1934 else: 

1935 return mi, ma, inc 

1936 

1937 def make_exp(self, x): 

1938 ''' 

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

1940 

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

1942 ''' 

1943 

1944 if self.exp is not None: 

1945 return self.exp 

1946 

1947 x = abs(x) 

1948 if x == 0.0: 

1949 return 0 

1950 

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

1952 return 0 

1953 

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

1955 

1956 def guess_autoscale_mode(self, data_min, data_max): 

1957 ''' 

1958 Guess mode of operation, based on data range. 

1959 

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

1961 or ``'symmetric'``. 

1962 ''' 

1963 

1964 a = 'min-max' 

1965 if data_min >= 0.0: 

1966 if data_min < data_max/2.: 

1967 a = '0-max' 

1968 else: 

1969 a = 'min-max' 

1970 if data_max <= 0.0: 

1971 if data_max > data_min/2.: 

1972 a = 'min-0' 

1973 else: 

1974 a = 'min-max' 

1975 if data_min < 0.0 and data_max > 0.0: 

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

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

1978 a = 'symmetric' 

1979 else: 

1980 a = 'min-max' 

1981 return a 

1982 

1983 

1984class Ax(AutoScaler): 

1985 ''' 

1986 Ax description with autoscaling capabilities. 

1987 

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

1989 the following additional attributes (with default values given in 

1990 paranthesis): 

1991 

1992 .. py:attribute:: label 

1993 

1994 Ax label (without unit). 

1995 

1996 .. py:attribute:: unit 

1997 

1998 Physical unit of the data attached to this ax. 

1999 

2000 .. py:attribute:: scaled_unit 

2001 

2002 (see below) 

2003 

2004 .. py:attribute:: scaled_unit_factor 

2005 

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

2007 

2008 unit = scaled_unit_factor x scaled_unit. 

2009 

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

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

2012 1e9.) 

2013 

2014 .. py:attribute:: limits 

2015 

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

2017 

2018 .. py:attribute:: masking 

2019 

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

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

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

2023 

2024 ''' 

2025 

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

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

2028 

2029 AutoScaler.__init__(self, **kwargs) 

2030 self.label = label 

2031 self.unit = unit 

2032 self.scaled_unit_factor = scaled_unit_factor 

2033 self.scaled_unit = scaled_unit 

2034 self.limits = limits 

2035 self.masking = masking 

2036 

2037 def label_str(self, exp, unit): 

2038 ''' 

2039 Get label string including the unit and multiplier. 

2040 ''' 

2041 

2042 slabel, sunit, sexp = '', '', '' 

2043 if self.label: 

2044 slabel = self.label 

2045 

2046 if unit or exp != 0: 

2047 if exp != 0: 

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

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

2050 else: 

2051 sunit = '[ %s ]' % unit 

2052 

2053 p = [] 

2054 if slabel: 

2055 p.append(slabel) 

2056 

2057 if sunit: 

2058 p.append(sunit) 

2059 

2060 return ' '.join(p) 

2061 

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

2063 override_scaled_unit_factor=None): 

2064 

2065 ''' 

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

2067 

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

2069 multiplier for given data range. 

2070 

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

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

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

2074 scaling applied. 

2075 ''' 

2076 

2077 sf = self.scaled_unit_factor 

2078 

2079 if override_scaled_unit_factor is not None: 

2080 sf = override_scaled_unit_factor 

2081 

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

2083 

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

2085 if self.inc is not None: 

2086 inc = self.inc*sf 

2087 

2088 if ax_projection: 

2089 exp = self.make_exp(inc) 

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

2091 unit = self.unit 

2092 else: 

2093 unit = self.scaled_unit 

2094 label = self.label_str(exp, unit) 

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

2096 else: 

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

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

2099 

2100 

2101class ScaleGuru(Guru): 

2102 

2103 ''' 

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

2105 

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

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

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

2109 arguments, which are required for most GMT commands. 

2110 

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

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

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

2114 

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

2116 

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

2118 limits imposed on other axes. 

2119 

2120 ''' 

2121 

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

2123 percent_interval=None, copy_from=None): 

2124 

2125 Guru.__init__(self) 

2126 

2127 if copy_from: 

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

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

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

2131 self.aspect = copy_from.aspect 

2132 

2133 if percent_interval is not None: 

2134 from scipy.stats import scoreatpercentile as scap 

2135 

2136 self.templates = dict( 

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

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

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

2140 

2141 maxdim = 2 

2142 if data_tuples: 

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

2144 else: 

2145 if axes: 

2146 maxdim = len(axes) 

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

2148 if axes is not None: 

2149 self.axes = axes 

2150 else: 

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

2152 

2153 # sophisticated data-range calculation 

2154 data_ranges = [None] * maxdim 

2155 for dt_ in data_tuples: 

2156 dt = num.asarray(dt_) 

2157 in_range = True 

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

2159 if ax.limits and ax.masking: 

2160 ax_limits = list(ax.limits) 

2161 if ax_limits[0] is None: 

2162 ax_limits[0] = -num.inf 

2163 if ax_limits[1] is None: 

2164 ax_limits[1] = num.inf 

2165 in_range = num.logical_and( 

2166 in_range, 

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

2168 

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

2170 

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

2172 if len(x) >= 1: 

2173 if in_range is not True: 

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

2175 if percent_interval is None: 

2176 range_this = ( 

2177 num.nanmin(xmasked), 

2178 num.nanmax(xmasked)) 

2179 else: 

2180 xmasked_finite = num.compress( 

2181 num.isfinite(xmasked), xmasked) 

2182 range_this = ( 

2183 scap(xmasked_finite, 

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

2185 scap(xmasked_finite, 

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

2187 else: 

2188 if percent_interval is None: 

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

2190 else: 

2191 xmasked_finite = num.compress( 

2192 num.isfinite(xmasked), xmasked) 

2193 range_this = ( 

2194 scap(xmasked_finite, 

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

2196 scap(xmasked_finite, 

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

2198 else: 

2199 range_this = (0., 1.) 

2200 

2201 if ax.limits: 

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

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

2204 range_this[1]) 

2205 

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

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

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

2209 

2210 else: 

2211 range_this = ax.limits 

2212 

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

2214 data_ranges[i] = range_this 

2215 else: 

2216 mi, ma = range_this 

2217 if data_ranges[i] is not None: 

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

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

2220 

2221 data_ranges[i] = (mi, ma) 

2222 

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

2224 if data_ranges[i] is None or not ( 

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

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

2227 

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

2229 

2230 self.data_ranges = data_ranges 

2231 self.aspect = aspect 

2232 

2233 def copy(self): 

2234 return ScaleGuru(copy_from=self) 

2235 

2236 def get_params(self, ax_projection=False): 

2237 

2238 ''' 

2239 Get dict with output parameters. 

2240 

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

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

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

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

2245 

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

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

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

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

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

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

2252 label string. 

2253 ''' 

2254 

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

2256 self.data_ranges[0], ax_projection) 

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

2258 self.data_ranges[1], ax_projection) 

2259 if len(self.axes) > 2: 

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

2261 self.data_ranges[2], ax_projection) 

2262 

2263 # enforce certain aspect, if needed 

2264 if self.aspect is not None: 

2265 xwid = xma-xmi 

2266 ywid = yma-ymi 

2267 if ywid < xwid*self.aspect: 

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

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

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

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

2272 override_scaled_unit_factor=1.) 

2273 

2274 elif xwid < ywid/self.aspect: 

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

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

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

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

2279 override_scaled_unit_factor=1.) 

2280 

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

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

2283 if len(self.axes) > 2: 

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

2285 

2286 return params 

2287 

2288 

2289class GumSpring(object): 

2290 

2291 ''' 

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

2293 ''' 

2294 

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

2296 self.minimal = minimal 

2297 if grow is None: 

2298 if minimal is None: 

2299 self.grow = 1.0 

2300 else: 

2301 self.grow = 0.0 

2302 else: 

2303 self.grow = grow 

2304 self.value = 1.0 

2305 

2306 def get_minimal(self): 

2307 if self.minimal is not None: 

2308 return self.minimal 

2309 else: 

2310 return 0.0 

2311 

2312 def get_grow(self): 

2313 return self.grow 

2314 

2315 def set_value(self, value): 

2316 self.value = value 

2317 

2318 def get_value(self): 

2319 return self.value 

2320 

2321 

2322def distribute(sizes, grows, space): 

2323 sizes = list(sizes) 

2324 gsum = sum(grows) 

2325 if gsum > 0.0: 

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

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

2328 return sizes 

2329 

2330 

2331class Widget(Guru): 

2332 

2333 ''' 

2334 Base class of the gmtpy layout system. 

2335 

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

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

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

2339 

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

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

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

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

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

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

2346 

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

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

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

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

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

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

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

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

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

2356 

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

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

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

2360 ''' 

2361 

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

2363 

2364 ''' 

2365 Create new widget. 

2366 ''' 

2367 

2368 Guru.__init__(self) 

2369 

2370 self.templates = dict( 

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

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

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

2374 

2375 if horizontal is None: 

2376 self.horizontal = GumSpring() 

2377 else: 

2378 self.horizontal = horizontal 

2379 

2380 if vertical is None: 

2381 self.vertical = GumSpring() 

2382 else: 

2383 self.vertical = vertical 

2384 

2385 self.aspect = None 

2386 self.parent = parent 

2387 self.dirty = True 

2388 

2389 def set_parent(self, parent): 

2390 

2391 ''' 

2392 Set the parent widget. 

2393 

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

2395 methods are responsible for calling this. 

2396 ''' 

2397 

2398 self.parent = parent 

2399 self.dirtyfy() 

2400 

2401 def get_parent(self): 

2402 

2403 ''' 

2404 Get the widgets parent widget. 

2405 ''' 

2406 

2407 return self.parent 

2408 

2409 def get_root(self): 

2410 

2411 ''' 

2412 Get the root widget in the layout hierarchy. 

2413 ''' 

2414 

2415 if self.parent is not None: 

2416 return self.get_parent() 

2417 else: 

2418 return self 

2419 

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

2421 

2422 ''' 

2423 Set the horizontal sizing policy of the Widget. 

2424 

2425 

2426 :param minimal: new minimal width of the widget 

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

2428 ''' 

2429 

2430 self.horizontal = GumSpring(minimal, grow) 

2431 self.dirtyfy() 

2432 

2433 def get_horizontal(self): 

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

2435 

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

2437 

2438 ''' 

2439 Set the horizontal sizing policy of the Widget. 

2440 

2441 :param minimal: new minimal height of the widget 

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

2443 ''' 

2444 

2445 self.vertical = GumSpring(minimal, grow) 

2446 self.dirtyfy() 

2447 

2448 def get_vertical(self): 

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

2450 

2451 def set_aspect(self, aspect=None): 

2452 

2453 ''' 

2454 Set aspect constraint on the widget. 

2455 

2456 The aspect is given as height divided by width. 

2457 ''' 

2458 

2459 self.aspect = aspect 

2460 self.dirtyfy() 

2461 

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

2463 

2464 ''' 

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

2466 call. 

2467 ''' 

2468 

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

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

2471 self.set_aspect(aspect) 

2472 

2473 def get_policy(self): 

2474 mh, gh = self.get_horizontal() 

2475 mv, gv = self.get_vertical() 

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

2477 

2478 def legalize(self, size, offset): 

2479 

2480 ''' 

2481 Get legal size for widget. 

2482 

2483 Returns: (new_size, new_offset) 

2484 

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

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

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

2488 ''' 

2489 

2490 sh, sv = size 

2491 oh, ov = offset 

2492 shs, svs = Widget.get_min_size(self) 

2493 ghs, gvs = Widget.get_grow(self) 

2494 

2495 if ghs == 0.0: 

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

2497 sh = shs 

2498 

2499 if gvs == 0.0: 

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

2501 sv = svs 

2502 

2503 if self.aspect is not None: 

2504 if sh > sv/self.aspect: 

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

2506 sh = sv/self.aspect 

2507 if sv > sh*self.aspect: 

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

2509 sv = sh*self.aspect 

2510 

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

2512 

2513 def get_min_size(self): 

2514 

2515 ''' 

2516 Get minimum size of widget. 

2517 

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

2519 ''' 

2520 

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

2522 if self.aspect is not None: 

2523 if mv == 0.0: 

2524 return mh, mh*self.aspect 

2525 elif mh == 0.0: 

2526 return mv/self.aspect, mv 

2527 return mh, mv 

2528 

2529 def get_grow(self): 

2530 

2531 ''' 

2532 Get widget's desire to grow. 

2533 

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

2535 ''' 

2536 

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

2538 

2539 def set_size(self, size, offset): 

2540 

2541 ''' 

2542 Set the widget's current size. 

2543 

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

2545 responsibility to call this. 

2546 ''' 

2547 

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

2549 self.offset = inner_offset 

2550 self.horizontal.set_value(sh) 

2551 self.vertical.set_value(sv) 

2552 self.dirty = False 

2553 

2554 def __str__(self): 

2555 

2556 def indent(ind, str): 

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

2558 size, offset = self.get_size() 

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

2560 children = self.get_children() 

2561 if children: 

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

2563 return s 

2564 

2565 def policies_debug_str(self): 

2566 

2567 def indent(ind, str): 

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

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

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

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

2572 

2573 children = self.get_children() 

2574 if children: 

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

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

2577 return s 

2578 

2579 def get_corners(self, descend=False): 

2580 

2581 ''' 

2582 Get coordinates of the corners of the widget. 

2583 

2584 Returns list with coordinate tuples. 

2585 

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

2587 coordinates of all sub-widgets. 

2588 ''' 

2589 

2590 self.do_layout() 

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

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

2593 if descend: 

2594 for child in self.get_children(): 

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

2596 return corners 

2597 

2598 def get_sizes(self): 

2599 

2600 ''' 

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

2602 

2603 Returns a list with size tuples. 

2604 ''' 

2605 self.do_layout() 

2606 sizes = [self.get_size()] 

2607 for child in self.get_children(): 

2608 sizes.extend(child.get_sizes()) 

2609 return sizes 

2610 

2611 def do_layout(self): 

2612 

2613 ''' 

2614 Triggers layouting of the widget hierarchy, if needed. 

2615 ''' 

2616 

2617 if self.parent is not None: 

2618 return self.parent.do_layout() 

2619 

2620 if not self.dirty: 

2621 return 

2622 

2623 sh, sv = self.get_min_size() 

2624 gh, gv = self.get_grow() 

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

2626 sh = 15.*cm 

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

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

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

2630 

2631 def get_children(self): 

2632 

2633 ''' 

2634 Get sub-widgets contained in this widget. 

2635 

2636 Returns a list of widgets. 

2637 ''' 

2638 

2639 return [] 

2640 

2641 def get_size(self): 

2642 

2643 ''' 

2644 Get current size and position of the widget. 

2645 

2646 Triggers layouting and returns 

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

2648 ''' 

2649 

2650 self.do_layout() 

2651 return (self.horizontal.get_value(), 

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

2653 

2654 def get_params(self): 

2655 

2656 ''' 

2657 Get current size and position of the widget. 

2658 

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

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

2661 ''' 

2662 

2663 self.do_layout() 

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

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

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

2667 

2668 def width(self): 

2669 

2670 ''' 

2671 Get current width of the widget. 

2672 

2673 Triggers layouting and returns width. 

2674 ''' 

2675 

2676 self.do_layout() 

2677 return self.horizontal.get_value() 

2678 

2679 def height(self): 

2680 

2681 ''' 

2682 Get current height of the widget. 

2683 

2684 Triggers layouting and return height. 

2685 ''' 

2686 

2687 self.do_layout() 

2688 return self.vertical.get_value() 

2689 

2690 def bbox(self): 

2691 

2692 ''' 

2693 Get PostScript bounding box for this widget. 

2694 

2695 Triggers layouting and returns values suitable to create PS bounding 

2696 box, representing the widgets current size and position. 

2697 ''' 

2698 

2699 self.do_layout() 

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

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

2702 

2703 def dirtyfy(self): 

2704 

2705 ''' 

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

2707 

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

2709 new layouting. 

2710 ''' 

2711 

2712 if self.parent is not None: 

2713 self.parent.dirtyfy() 

2714 

2715 self.dirty = True 

2716 

2717 

2718class CenterLayout(Widget): 

2719 

2720 ''' 

2721 A layout manager which centers its single child widget. 

2722 

2723 The child widget may be oversized. 

2724 ''' 

2725 

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

2727 Widget.__init__(self, horizontal, vertical) 

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

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

2730 

2731 def get_min_size(self): 

2732 shs, svs = Widget.get_min_size(self) 

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

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

2735 

2736 def get_grow(self): 

2737 ghs, gvs = Widget.get_grow(self) 

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

2739 return gh*ghs, gv*gvs 

2740 

2741 def set_size(self, size, offset): 

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

2743 

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

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

2746 if ghc != 0.: 

2747 shc = sh 

2748 if gvc != 0.: 

2749 svc = sv 

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

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

2752 

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

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

2755 

2756 def set_widget(self, widget=None): 

2757 

2758 ''' 

2759 Set the child widget, which shall be centered. 

2760 ''' 

2761 

2762 if widget is None: 

2763 widget = Widget() 

2764 

2765 self.content = widget 

2766 

2767 widget.set_parent(self) 

2768 

2769 def get_widget(self): 

2770 return self.content 

2771 

2772 def get_children(self): 

2773 return [self.content] 

2774 

2775 

2776class FrameLayout(Widget): 

2777 

2778 ''' 

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

2780 widgets. 

2781 

2782 :: 

2783 

2784 +---------------------------+ 

2785 | top | 

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

2787 | | | | 

2788 | left | center | right | 

2789 | | | | 

2790 +---------------------------+ 

2791 | bottom | 

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

2793 

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

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

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

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

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

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

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

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

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

2803 spaces between the widgets. 

2804 ''' 

2805 

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

2807 Widget.__init__(self, horizontal, vertical) 

2808 mw = 3.*cm 

2809 self.left = Widget( 

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

2811 self.right = Widget( 

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

2813 self.top = Widget( 

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

2815 parent=self) 

2816 self.bottom = Widget( 

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

2818 parent=self) 

2819 self.center = Widget( 

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

2821 parent=self) 

2822 

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

2824 ''' 

2825 Give margins fixed size constraints. 

2826 ''' 

2827 

2828 self.left.set_horizontal(left, 0) 

2829 self.right.set_horizontal(right, 0) 

2830 self.top.set_vertical(top, 0) 

2831 self.bottom.set_vertical(bottom, 0) 

2832 

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

2834 ''' 

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

2836 

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

2838 ''' 

2839 self.left.set_horizontal(left, grow) 

2840 self.right.set_horizontal(right, grow) 

2841 self.top.set_vertical(top, grow) 

2842 self.bottom.set_vertical(bottom, grow) 

2843 

2844 def get_min_size(self): 

2845 shs, svs = Widget.get_min_size(self) 

2846 

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

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

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

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

2851 

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

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

2854 

2855 # prevent widgets from collapsing 

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

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

2858 shsum += 0.1*cm 

2859 

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

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

2862 svsum += 0.1*cm 

2863 

2864 sh = max(shs, shsum) 

2865 sv = max(svs, svsum) 

2866 

2867 return sh, sv 

2868 

2869 def get_grow(self): 

2870 ghs, gvs = Widget.get_grow(self) 

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

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

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

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

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

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

2877 return gh, gv 

2878 

2879 def set_size(self, size, offset): 

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

2881 

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

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

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

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

2886 

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

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

2889 

2890 if ah < 0.0: 

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

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

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

2894 if av < 0.0: 

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

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

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

2898 

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

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

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

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

2903 

2904 if self.center.aspect is not None: 

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

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

2907 if 0.0 < ahm < ah: 

2908 slh, srh, sch = distribute( 

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

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

2911 

2912 elif 0.0 < avm < av: 

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

2914 sch*self.center.aspect), 

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

2916 

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

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

2919 

2920 oh += ah/2. 

2921 ov += av/2. 

2922 sh -= ah 

2923 sv -= av 

2924 

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

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

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

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

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

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

2931 

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

2933 

2934 ''' 

2935 Set one of the sub-widgets. 

2936 

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

2938 ``'bottom'`` or ``'center'``. 

2939 ''' 

2940 

2941 if widget is None: 

2942 widget = Widget() 

2943 

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

2945 self.__dict__[which] = widget 

2946 else: 

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

2948 

2949 widget.set_parent(self) 

2950 

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

2952 

2953 ''' 

2954 Get one of the sub-widgets. 

2955 

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

2957 ``'bottom'`` or ``'center'``. 

2958 ''' 

2959 

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

2961 return self.__dict__[which] 

2962 else: 

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

2964 

2965 def get_children(self): 

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

2967 

2968 

2969class GridLayout(Widget): 

2970 

2971 ''' 

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

2973 

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

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

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

2977 

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

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

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

2981 might not be resolved optimally. 

2982 ''' 

2983 

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

2985 

2986 ''' 

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

2988 ''' 

2989 

2990 Widget.__init__(self, horizontal, vertical) 

2991 self.grid = [] 

2992 for iy in range(ny): 

2993 row = [] 

2994 for ix in range(nx): 

2995 w = Widget(parent=self) 

2996 row.append(w) 

2997 

2998 self.grid.append(row) 

2999 

3000 def sub_min_sizes_as_array(self): 

3001 esh = num.array( 

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

3003 dtype=float) 

3004 esv = num.array( 

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

3006 dtype=float) 

3007 return esh, esv 

3008 

3009 def sub_grows_as_array(self): 

3010 egh = num.array( 

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

3012 dtype=float) 

3013 egv = num.array( 

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

3015 dtype=float) 

3016 return egh, egv 

3017 

3018 def get_min_size(self): 

3019 sh, sv = Widget.get_min_size(self) 

3020 esh, esv = self.sub_min_sizes_as_array() 

3021 if esh.size != 0: 

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

3023 if esv.size != 0: 

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

3025 return sh, sv 

3026 

3027 def get_grow(self): 

3028 ghs, gvs = Widget.get_grow(self) 

3029 egh, egv = self.sub_grows_as_array() 

3030 if egh.size != 0: 

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

3032 else: 

3033 gh = 1.0 

3034 if egv.size != 0: 

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

3036 else: 

3037 gv = 1.0 

3038 return gh, gv 

3039 

3040 def set_size(self, size, offset): 

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

3042 esh, esv = self.sub_min_sizes_as_array() 

3043 egh, egv = self.sub_grows_as_array() 

3044 

3045 # available additional space 

3046 empty = esh.size == 0 

3047 

3048 if not empty: 

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

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

3051 else: 

3052 av = sv 

3053 ah = sh 

3054 

3055 if ah < 0.0: 

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

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

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

3059 if av < 0.0: 

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

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

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

3063 

3064 nx, ny = esh.shape 

3065 

3066 if not empty: 

3067 # distribute additional space on rows and columns 

3068 # according to grow weights and minimal sizes 

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

3070 nesh = esh.copy() 

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

3072 

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

3074 

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

3076 nesv = esv.copy() 

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

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

3079 

3080 ah = sh - sum(nsh) 

3081 av = sv - sum(nsv) 

3082 

3083 oh += ah/2. 

3084 ov += av/2. 

3085 sh -= ah 

3086 sv -= av 

3087 

3088 # resize child widgets 

3089 neov = ov + sum(nsv) 

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

3091 neov -= nesv 

3092 neoh = oh 

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

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

3095 neoh += nesh 

3096 

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

3098 

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

3100 

3101 ''' 

3102 Set one of the sub-widgets. 

3103 

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

3105 counted from zero. 

3106 ''' 

3107 

3108 if widget is None: 

3109 widget = Widget() 

3110 

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

3112 widget.set_parent(self) 

3113 

3114 def get_widget(self, ix, iy): 

3115 

3116 ''' 

3117 Get one of the sub-widgets. 

3118 

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

3120 counted from zero. 

3121 ''' 

3122 

3123 return self.grid[iy][ix] 

3124 

3125 def get_children(self): 

3126 children = [] 

3127 for row in self.grid: 

3128 children.extend(row) 

3129 

3130 return children 

3131 

3132 

3133def is_gmt5(version='newest'): 

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

3135 

3136 

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

3138 

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

3140 

3141 if gmt.is_gmt5(): 

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

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

3144 gmt.save(fn, crop_eps_mode=True) 

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

3146 s = f.read() 

3147 

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

3149 else: 

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

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

3152 

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

3154 

3155 

3156def text_box( 

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

3158 

3159 gmt = GMT(version=gmtversion) 

3160 if gmt.is_gmt5(): 

3161 row = [0, 0, text] 

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

3163 else: 

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

3165 farg = [] 

3166 

3167 gmt.pstext( 

3168 in_rows=[row], 

3169 finish=True, 

3170 R=(0, 1, 0, 1), 

3171 J='x10p', 

3172 N=True, 

3173 *farg, 

3174 **kwargs) 

3175 

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

3177 gmt.save(fn) 

3178 

3179 (_, stderr) = subprocess.Popen( 

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

3181 stderr=subprocess.PIPE).communicate() 

3182 

3183 dx, dy = None, None 

3184 for line in stderr.splitlines(): 

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

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

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

3188 break 

3189 

3190 return dx, dy 

3191 

3192 

3193class TableLiner(object): 

3194 ''' 

3195 Utility class to turn tables into lines. 

3196 ''' 

3197 

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

3199 self.in_columns = in_columns 

3200 self.in_rows = in_rows 

3201 self.encoding = encoding 

3202 

3203 def __iter__(self): 

3204 if self.in_columns is not None: 

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

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

3207 self.encoding) 

3208 

3209 if self.in_rows is not None: 

3210 for row in self.in_rows: 

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

3212 self.encoding) 

3213 

3214 

3215class LineStreamChopper(object): 

3216 ''' 

3217 File-like object to buffer data. 

3218 ''' 

3219 

3220 def __init__(self, liner): 

3221 self.chopsize = None 

3222 self.liner = liner 

3223 self.chop_iterator = None 

3224 self.closed = False 

3225 

3226 def _chopiter(self): 

3227 buf = BytesIO() 

3228 for line in self.liner: 

3229 buf.write(line) 

3230 buflen = buf.tell() 

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

3232 buf.seek(0) 

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

3234 yield buf.read(self.chopsize) 

3235 

3236 newbuf = BytesIO() 

3237 newbuf.write(buf.read()) 

3238 buf.close() 

3239 buf = newbuf 

3240 

3241 yield(buf.getvalue()) 

3242 buf.close() 

3243 

3244 def read(self, size=None): 

3245 if self.closed: 

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

3247 if self.chop_iterator is None: 

3248 self.chopsize = size 

3249 self.chop_iterator = self._chopiter() 

3250 

3251 self.chopsize = size 

3252 try: 

3253 return next(self.chop_iterator) 

3254 except StopIteration: 

3255 return '' 

3256 

3257 def close(self): 

3258 self.chopsize = None 

3259 self.chop_iterator = None 

3260 self.closed = True 

3261 

3262 def flush(self): 

3263 pass 

3264 

3265 

3266font_tab = { 

3267 0: 'Helvetica', 

3268 1: 'Helvetica-Bold', 

3269} 

3270 

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

3272 

3273 

3274class GMT(object): 

3275 ''' 

3276 A thin wrapper to GMT command execution. 

3277 

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

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

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

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

3282 gmtpy and gmtpy must know where to find it. 

3283 

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

3285 output file. 

3286 

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

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

3289 

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

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

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

3293 

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

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

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

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

3298 

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

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

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

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

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

3304 execution of more than one GMT instance. 

3305 

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

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

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

3309 backward compatibility of the scripts can be maintained. 

3310 

3311 ''' 

3312 

3313 def __init__( 

3314 self, 

3315 config=None, 

3316 kontinue=None, 

3317 version='newest', 

3318 config_papersize=None, 

3319 eps_mode=False): 

3320 

3321 self.installation = get_gmt_installation(version) 

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

3323 self.eps_mode = eps_mode 

3324 self._shutil = shutil 

3325 

3326 if config: 

3327 self.gmt_config.update(config) 

3328 

3329 if config_papersize: 

3330 if not isinstance(config_papersize, str): 

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

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

3333 

3334 if self.is_gmt5(): 

3335 self.gmt_config['PS_MEDIA'] = config_papersize 

3336 else: 

3337 self.gmt_config['PAPER_MEDIA'] = config_papersize 

3338 

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

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

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

3342 

3343 if kontinue is not None: 

3344 self.load_unfinished(kontinue) 

3345 self.needstart = False 

3346 else: 

3347 self.output = BytesIO() 

3348 self.needstart = True 

3349 

3350 self.finished = False 

3351 

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

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

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

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

3356 

3357 self.layout = None 

3358 self.command_log = [] 

3359 self.keep_temp_dir = False 

3360 

3361 def is_gmt5(self): 

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

3363 

3364 def get_version(self): 

3365 return self.installation['version'] 

3366 

3367 def get_config(self, key): 

3368 return self.gmt_config[key] 

3369 

3370 def to_points(self, string): 

3371 if not string: 

3372 return 0 

3373 

3374 unit = string[-1] 

3375 if unit in _units: 

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

3377 else: 

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

3379 return float(string)/_units[default_unit] 

3380 

3381 def label_font_size(self): 

3382 if self.is_gmt5(): 

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

3384 else: 

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

3386 

3387 def label_font(self): 

3388 if self.is_gmt5(): 

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

3390 else: 

3391 return self.gmt_config['LABEL_FONT'] 

3392 

3393 def gen_gmt_config_file(self, config_filename, config): 

3394 f = open(config_filename, 'wb') 

3395 f.write( 

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

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

3398 

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

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

3401 f.close() 

3402 

3403 def __del__(self): 

3404 if not self.keep_temp_dir: 

3405 self._shutil.rmtree(self.tempdir) 

3406 

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

3408 

3409 ''' 

3410 Execute arbitrary GMT command. 

3411 

3412 See docstring in __getattr__ for details. 

3413 ''' 

3414 

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

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

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

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

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

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

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

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

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

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

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

3426 

3427 assert(not self.finished) 

3428 

3429 # check for mutual exclusiveness on input and output possibilities 

3430 assert(1 >= len( 

3431 [x for x in [ 

3432 in_stream, in_filename, in_string, in_columns, in_rows] 

3433 if x is not None])) 

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

3435 if x is not None])) 

3436 

3437 options = [] 

3438 

3439 gmt_config = self.gmt_config 

3440 if not self.is_gmt5(): 

3441 gmt_config_filename = self.gmt_config_filename 

3442 if config_override: 

3443 gmt_config = self.gmt_config.copy() 

3444 gmt_config.update(config_override) 

3445 gmt_config_override_filename = pjoin( 

3446 self.tempdir, 'gmtdefaults_override') 

3447 self.gen_gmt_config_file( 

3448 gmt_config_override_filename, gmt_config) 

3449 gmt_config_filename = gmt_config_override_filename 

3450 

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

3452 if config_override: 

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

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

3455 

3456 if out_discard: 

3457 out_filename = '/dev/null' 

3458 

3459 out_mustclose = False 

3460 if out_filename is not None: 

3461 out_mustclose = True 

3462 out_stream = open(out_filename, 'wb') 

3463 

3464 if in_filename is not None: 

3465 in_stream = open(in_filename, 'rb') 

3466 

3467 if in_string is not None: 

3468 in_stream = BytesIO(in_string) 

3469 

3470 encoding_gmt = gmt_config.get( 

3471 'PS_CHAR_ENCODING', 

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

3473 

3474 encoding = encoding_gmt_to_python[encoding_gmt.lower()] 

3475 

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

3477 in_stream = LineStreamChopper(TableLiner(in_columns=in_columns, 

3478 in_rows=in_rows, 

3479 encoding=encoding)) 

3480 

3481 # convert option arguments to strings 

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

3483 if len(k) > 1: 

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

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

3486 % (k, command)) 

3487 

3488 if type(v) is bool: 

3489 if v: 

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

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

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

3493 else: 

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

3495 

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

3497 if out_stream is None: 

3498 if not finish: 

3499 options.append('-K') 

3500 else: 

3501 self.finished = True 

3502 

3503 if not self.needstart: 

3504 options.append('-O') 

3505 else: 

3506 self.needstart = False 

3507 

3508 out_stream = self.output 

3509 

3510 # run the command 

3511 if self.is_gmt5(): 

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

3513 else: 

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

3515 

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

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

3518 args.extend(options) 

3519 args.extend(addargs) 

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

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

3522 args.append('+'+gmt_config_filename) 

3523 

3524 bs = 2048 

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

3526 stdout=subprocess.PIPE, bufsize=bs, 

3527 env=self.environ) 

3528 while True: 

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

3530 if cr: 

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

3532 if cw: 

3533 if in_stream is not None: 

3534 data = in_stream.read(bs) 

3535 if len(data) == 0: 

3536 break 

3537 p.stdin.write(data) 

3538 else: 

3539 break 

3540 if not cr and not cw: 

3541 break 

3542 

3543 p.stdin.close() 

3544 

3545 while True: 

3546 data = p.stdout.read(bs) 

3547 if len(data) == 0: 

3548 break 

3549 out_stream.write(data) 

3550 

3551 p.stdout.close() 

3552 

3553 retcode = p.wait() 

3554 

3555 if in_stream is not None: 

3556 in_stream.close() 

3557 

3558 if out_mustclose: 

3559 out_stream.close() 

3560 

3561 if retcode != 0: 

3562 self.keep_temp_dir = True 

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

3564 'While executing command:\n%s' 

3565 % (command, escape_shell_args(args))) 

3566 

3567 self.command_log.append(args) 

3568 

3569 def __getattr__(self, command): 

3570 

3571 ''' 

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

3573 

3574 Execute arbitrary GMT command. 

3575 

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

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

3578 called. 

3579 

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

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

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

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

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

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

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

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

3588 

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

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

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

3592 not interested in the output. 

3593 

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

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

3596 

3597 =============== ======================================================= 

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

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

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

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

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

3603 ascii 

3604 table, which is fed to the process. 

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

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

3607 table, which is fed to the process. 

3608 =============== ======================================================= 

3609 

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

3611 following options: 

3612 

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

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

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

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

3617 ================= ===================================================== 

3618 

3619 Additional keyword arguments: 

3620 

3621 ===================== ================================================= 

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

3623 currently active set of defaults exclusively 

3624 during this call. 

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

3626 by the GMT instance is finished, and no further 

3627 plotting is allowed. 

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

3629 option to the command. 

3630 ===================== ================================================= 

3631 

3632 ''' 

3633 

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

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

3636 return f 

3637 

3638 def tempfilename(self, name=None): 

3639 ''' 

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

3641 

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

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

3644 ''' 

3645 

3646 if not name: 

3647 name = ''.join( 

3648 [random.choice('abcdefghijklmnopqrstuvwxyz') 

3649 for i in range(10)]) 

3650 

3651 fn = pjoin(self.tempdir, name) 

3652 return fn 

3653 

3654 def tempfile(self, name=None): 

3655 ''' 

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

3657 ''' 

3658 

3659 fn = self.tempfilename(name) 

3660 f = open(fn, 'wb') 

3661 return f, fn 

3662 

3663 def save_unfinished(self, filename): 

3664 out = open(filename, 'wb') 

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

3666 out.close() 

3667 

3668 def load_unfinished(self, filename): 

3669 self.output = BytesIO() 

3670 self.finished = False 

3671 inp = open(filename, 'rb') 

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

3673 inp.close() 

3674 

3675 def dump(self, ident): 

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

3677 self.save_unfinished(filename) 

3678 

3679 def load(self, ident): 

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

3681 self.load_unfinished(filename) 

3682 

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

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

3685 psconvert=False): 

3686 

3687 ''' 

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

3689 

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

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

3692 

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

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

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

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

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

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

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

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

3701 

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

3703 ''' 

3704 

3705 if not self.finished: 

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

3707 

3708 if filename: 

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

3710 out = open(tempfn, 'wb') 

3711 else: 

3712 out = sys.stdout 

3713 

3714 if bbox and not self.is_gmt5(): 

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

3716 else: 

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

3718 

3719 if filename: 

3720 out.close() 

3721 

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

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

3724 

3725 shutil.move(tempfn, filename) 

3726 return 

3727 

3728 if self.is_gmt5(): 

3729 if crop_eps_mode: 

3730 addarg = ['-A'] 

3731 else: 

3732 addarg = [] 

3733 

3734 subprocess.call( 

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

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

3737 

3738 if bbox: 

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

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

3741 replace_bbox(bbox, fin, fout) 

3742 

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

3744 

3745 else: 

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

3747 

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

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

3750 return 

3751 

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

3753 if psconvert: 

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

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

3756 '-F' + filename]) 

3757 else: 

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

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

3760 else: 

3761 subprocess.call([ 

3762 'gmtpy-epstopdf', 

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

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

3765 

3766 convert_graph( 

3767 tempfn + '.pdf', filename, 

3768 resolution=resolution, oversample=oversample, 

3769 size=size, width=width, height=height) 

3770 

3771 def bbox(self): 

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

3773 

3774 def get_command_log(self): 

3775 ''' 

3776 Get the command log. 

3777 ''' 

3778 

3779 return self.command_log 

3780 

3781 def __str__(self): 

3782 s = '' 

3783 for com in self.command_log: 

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

3785 return s 

3786 

3787 def page_size_points(self): 

3788 ''' 

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

3790 ''' 

3791 

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

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

3794 pm = pm[:-1] 

3795 

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

3797 

3798 if pm in all_paper_sizes(): 

3799 

3800 if orient == 'portrait': 

3801 return get_paper_size(pm) 

3802 else: 

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

3804 

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

3806 if m: 

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

3808 w, h = float(w), float(h) 

3809 if uw: 

3810 w *= _units[uw] 

3811 if uh: 

3812 h *= _units[uh] 

3813 if orient == 'portrait': 

3814 return w, h 

3815 else: 

3816 return h, w 

3817 

3818 return None, None 

3819 

3820 def default_layout(self, with_palette=False): 

3821 ''' 

3822 Get a default layout for the output page. 

3823 

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

3825 `PAPER_MEDIA` setting in the GMT configuration dict. 

3826 

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

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

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

3830 :py:class:`FrameLayout`. 

3831 

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

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

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

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

3836 

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

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

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

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

3841 is preserved. 

3842 

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

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

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

3846 ''' 

3847 

3848 if self.layout is None: 

3849 w, h = self.page_size_points() 

3850 

3851 if w is None or h is None: 

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

3853 

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

3855 

3856 if with_palette: 

3857 palette_layout = GridLayout(3, 1) 

3858 spacer = palette_layout.get_widget(1, 0) 

3859 palette_widget = palette_layout.get_widget(2, 0) 

3860 spacer.set_horizontal(0.5*cm) 

3861 palette_widget.set_horizontal(0.5*cm) 

3862 

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

3864 outer = CenterLayout() 

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

3866 inner = FrameLayout() 

3867 outer.set_widget(inner) 

3868 if with_palette: 

3869 inner.set_widget('center', palette_layout) 

3870 widget = palette_layout 

3871 else: 

3872 widget = inner.get_widget('center') 

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

3874 aspect=1./golden_ratio) 

3875 mw = 3.0*cm 

3876 inner.set_fixed_margins( 

3877 mw, mw, mw/golden_ratio, mw/golden_ratio) 

3878 self.layout = inner 

3879 

3880 elif pm.startswith('custom_'): 

3881 layout = FrameLayout() 

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

3883 mw = 3.0*cm 

3884 layout.set_min_margins( 

3885 mw, mw, mw/golden_ratio, mw/golden_ratio) 

3886 if with_palette: 

3887 layout.set_widget('center', palette_layout) 

3888 self.layout = layout 

3889 else: 

3890 outer = FrameLayout() 

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

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

3893 

3894 inner = FrameLayout() 

3895 outer.set_widget('center', inner) 

3896 mw = 3.0*cm 

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

3898 if with_palette: 

3899 inner.set_widget('center', palette_layout) 

3900 widget = palette_layout 

3901 else: 

3902 widget = inner.get_widget('center') 

3903 

3904 widget.set_aspect(1./golden_ratio) 

3905 

3906 self.layout = inner 

3907 

3908 return self.layout 

3909 

3910 def draw_layout(self, layout): 

3911 ''' 

3912 Use psxy to draw layout; for debugging 

3913 ''' 

3914 

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

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

3917 rects_wid = rects[:, 0, 0] 

3918 rects_hei = rects[:, 0, 1] 

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

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

3921 nrects = len(rects) 

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

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

3924 

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

3926 

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

3928 self.makecpt( 

3929 C='ocean', 

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

3931 Z=True, 

3932 out_filename=cptfile, suppress_defaults=True) 

3933 

3934 bb = layout.bbox() 

3935 self.psxy( 

3936 in_columns=prects, 

3937 C=cptfile, 

3938 W='1p', 

3939 S='J', 

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

3941 *layout.XYJ()) 

3942 

3943 

3944def simpleconf_to_ax(conf, axname): 

3945 c = {} 

3946 x = axname 

3947 for x in ('', axname): 

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

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

3950 'snap'): 

3951 

3952 if x+k in conf: 

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

3954 

3955 return Ax(**c) 

3956 

3957 

3958class DensityPlotDef(object): 

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

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

3961 self.data = data 

3962 self.cpt = cpt 

3963 self.tension = tension 

3964 self.size = size 

3965 self.contour = contour 

3966 self.method = method 

3967 self.zscaler = zscaler 

3968 self.extra = extra 

3969 

3970 

3971class TextDef(object): 

3972 def __init__( 

3973 self, 

3974 data, 

3975 size=9, 

3976 justify='MC', 

3977 fontno=0, 

3978 offset=(0, 0), 

3979 color='black'): 

3980 

3981 self.data = data 

3982 self.size = size 

3983 self.justify = justify 

3984 self.fontno = fontno 

3985 self.offset = offset 

3986 self.color = color 

3987 

3988 

3989class Simple(object): 

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

3991 self.data = [] 

3992 self.symbols = [] 

3993 self.config = copy.deepcopy(simple_config) 

3994 self.gmtconfig = gmtconfig 

3995 self.density_plot_defs = [] 

3996 self.text_defs = [] 

3997 

3998 self.gmtversion = gmtversion 

3999 

4000 self.data_x = [] 

4001 self.symbols_x = [] 

4002 

4003 self.data_y = [] 

4004 self.symbols_y = [] 

4005 

4006 self.default_config = {} 

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

4008 height=15.*cm / golden_ratio, 

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

4010 with_palette=False, 

4011 palette_offset=0.5*cm, 

4012 palette_width=None, 

4013 palette_height=None, 

4014 zlabeloffset=2*cm, 

4015 draw_layout=False) 

4016 

4017 self.setup_defaults() 

4018 self.fixate_widget_aspect = False 

4019 

4020 def setup_defaults(self): 

4021 pass 

4022 

4023 def set_defaults(self, **kwargs): 

4024 self.default_config.update(kwargs) 

4025 

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

4027 self.data.append(data) 

4028 self.symbols.append(symbol) 

4029 

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

4031 dpd = DensityPlotDef(data, **kwargs) 

4032 self.density_plot_defs.append(dpd) 

4033 

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

4035 dpd = TextDef(data, **kwargs) 

4036 self.text_defs.append(dpd) 

4037 

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

4039 self.data_x.append(data) 

4040 self.symbols_x.append(symbol) 

4041 

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

4043 self.data_y.append(data) 

4044 self.symbols_y.append(symbol) 

4045 

4046 def set(self, **kwargs): 

4047 self.config.update(kwargs) 

4048 

4049 def setup_base(self, conf): 

4050 w = conf.pop('width') 

4051 h = conf.pop('height') 

4052 margins = conf.pop('margins') 

4053 

4054 gmtconfig = {} 

4055 if self.gmtconfig is not None: 

4056 gmtconfig.update(self.gmtconfig) 

4057 

4058 gmt = GMT( 

4059 version=self.gmtversion, 

4060 config=gmtconfig, 

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

4062 

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

4064 layout.set_min_margins(*margins) 

4065 if conf['with_palette']: 

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

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

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

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

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

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

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

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

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

4075 return gmt, layout, widget, palette_widget 

4076 else: 

4077 widget = layout.get_widget() 

4078 return gmt, layout, widget, None 

4079 

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

4081 pass 

4082 

4083 def setup_scaling(self, conf): 

4084 ndims = 2 

4085 if self.density_plot_defs: 

4086 ndims = 3 

4087 

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

4089 

4090 data_all = [] 

4091 data_all.extend(self.data) 

4092 for dsd in self.density_plot_defs: 

4093 if dsd.zscaler is None: 

4094 data_all.append(dsd.data) 

4095 else: 

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

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

4098 

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

4100 

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

4102 

4103 return scaler 

4104 

4105 def setup_scaling_plus(self, scaler, axes): 

4106 pass 

4107 

4108 def setup_scaling_extra(self, scaler, conf): 

4109 

4110 scaler_x = scaler.copy() 

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

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

4113 

4114 scaler_y = scaler.copy() 

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

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

4117 

4118 return scaler_x, scaler_y 

4119 

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

4121 

4122 R = scaler.R() 

4123 # par = scaler.get_params() 

4124 rxyj = R + widget.XYJ() 

4125 innerticks = False 

4126 for dpd in self.density_plot_defs: 

4127 

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

4129 

4130 if dpd.zscaler is not None: 

4131 s = dpd.zscaler 

4132 else: 

4133 s = scaler 

4134 

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

4136 

4137 fn_grid = gmt.tempfilename() 

4138 

4139 fn_mean = gmt.tempfilename() 

4140 

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

4142 gmt.blockmean(in_columns=dpd.data, 

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

4144 out_filename=fn_mean, *R) 

4145 

4146 if dpd.method == 'surface': 

4147 gmt.surface( 

4148 in_filename=fn_mean, 

4149 T=dpd.tension, 

4150 G=fn_grid, 

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

4152 out_discard=True, 

4153 *R) 

4154 

4155 if dpd.method == 'triangulate': 

4156 gmt.triangulate( 

4157 in_filename=fn_mean, 

4158 G=fn_grid, 

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

4160 out_discard=True, 

4161 V=True, 

4162 *R) 

4163 

4164 if gmt.is_gmt5(): 

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

4166 

4167 else: 

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

4169 

4170 if dpd.contour: 

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

4172 innerticks = '0.5p,black' 

4173 

4174 os.remove(fn_grid) 

4175 os.remove(fn_mean) 

4176 

4177 if dpd.method == 'fillcontour': 

4178 extra = dict(C=fn_cpt) 

4179 extra.update(dpd.extra) 

4180 gmt.pscontour(in_columns=dpd.data, 

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

4182 

4183 if dpd.method == 'contour': 

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

4185 extra.update(dpd.extra) 

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

4187 

4188 return fn_cpt, innerticks 

4189 

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

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

4192 

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

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

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

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

4197 

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

4199 pass 

4200 

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

4202 pass 

4203 

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

4205 

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

4207 gmt.psxy(in_columns=dat, 

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

4209 

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

4211 gmt.psxy(in_columns=dat, 

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

4213 

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

4215 

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

4217 for td in self.text_defs: 

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

4219 text = td.data[-1] 

4220 size = td.size 

4221 angle = 0 

4222 fontno = td.fontno 

4223 justify = td.justify 

4224 color = td.color 

4225 if gmt.is_gmt5(): 

4226 gmt.pstext( 

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

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

4229 size, fontno, color, angle, justify), 

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

4231 else: 

4232 gmt.pstext( 

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

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

4235 

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

4237 

4238 conf = dict(self.default_config) 

4239 conf.update(self.config) 

4240 

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

4242 scaler = self.setup_scaling(conf) 

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

4244 

4245 self.setup_projection(widget, scaler, conf) 

4246 if self.fixate_widget_aspect: 

4247 aspect = aspect_for_projection( 

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

4249 

4250 widget.set_aspect(aspect) 

4251 

4252 if conf['draw_layout']: 

4253 gmt.draw_layout(layout) 

4254 cptfile = None 

4255 if self.density_plot_defs: 

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

4257 self.pre_draw(gmt, widget, scaler) 

4258 self.draw(gmt, widget, scaler) 

4259 self.post_draw(gmt, widget, scaler) 

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

4261 self.draw_text(gmt, widget, scaler) 

4262 self.draw_basemap(gmt, widget, scaler) 

4263 

4264 if palette_widget and cptfile: 

4265 nice_palette(gmt, palette_widget, scaler, cptfile, 

4266 innerticks=innerticks, 

4267 zlabeloffset=conf['zlabeloffset']) 

4268 

4269 gmt.save(filename, resolution=resolution) 

4270 

4271 

4272class LinLinPlot(Simple): 

4273 pass 

4274 

4275 

4276class LogLinPlot(Simple): 

4277 

4278 def setup_defaults(self): 

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

4280 

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

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

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

4284 

4285 

4286class LinLogPlot(Simple): 

4287 

4288 def setup_defaults(self): 

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

4290 

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

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

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

4294 

4295 

4296class LogLogPlot(Simple): 

4297 

4298 def setup_defaults(self): 

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

4300 

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

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

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

4304 

4305 

4306class AziDistPlot(Simple): 

4307 

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

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

4310 self.fixate_widget_aspect = True 

4311 

4312 def setup_defaults(self): 

4313 self.set_defaults( 

4314 height=15.*cm, 

4315 width=15.*cm, 

4316 xmode='off', 

4317 xlimits=(0., 360.), 

4318 xinc=45.) 

4319 

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

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

4322 

4323 def setup_scaling_plus(self, scaler, axes): 

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

4325 

4326 

4327class MPlot(Simple): 

4328 

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

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

4331 self.fixate_widget_aspect = True 

4332 

4333 def setup_defaults(self): 

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

4335 

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

4337 par = scaler.get_params() 

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

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

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

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

4342 scaler['B'] = \ 

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

4344 

4345 

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

4347 innerticks=True): 

4348 

4349 par = scaleguru.get_params() 

4350 par_ax = scaleguru.get_params(ax_projection=True) 

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

4352 px = num.zeros(nz_palette*2) 

4353 px[1::2] += 1 

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

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

4356 palgrdfile = gmt.tempfilename() 

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

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

4359 gmt.xyz2grd( 

4360 G=palgrdfile, R=pal_r, 

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

4362 out_discard=True) 

4363 

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

4365 if isinstance(innerticks, str): 

4366 tickpen = innerticks 

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

4368 *widget.JXY()) 

4369 

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

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

4372 ticklen = negpalwid 

4373 else: 

4374 ticklen = '0p' 

4375 

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

4377 gmt.psbasemap( 

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

4379 config={TICK_LENGTH_PARAM: ticklen}, 

4380 *widget.JXY()) 

4381 

4382 if innerticks: 

4383 gmt.psbasemap( 

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

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

4386 *widget.JXY()) 

4387 else: 

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

4389 

4390 if par_ax['zlabel']: 

4391 label_font = gmt.label_font() 

4392 label_font_size = gmt.label_font_size() 

4393 label_offset = zlabeloffset 

4394 gmt.pstext( 

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

4396 N=True, 

4397 in_rows=[(1, 1, label_font_size, -90, label_font, 'CB', 

4398 par_ax['zlabel'])], 

4399 *widget.JXY())