1# http://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

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

5''' 

6A Python interface to GMT. 

7''' 

8 

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

10# See there for copying and licensing information. 

11 

12from __future__ import print_function, absolute_import 

13import subprocess 

14try: 

15 from StringIO import StringIO as BytesIO 

16except ImportError: 

17 from io import BytesIO 

18import re 

19import os 

20import sys 

21import shutil 

22from os.path import join as pjoin 

23import tempfile 

24import random 

25import logging 

26import math 

27import numpy as num 

28import copy 

29from select import select 

30from scipy.io import netcdf 

31 

32from pyrocko import ExternalProgramMissing 

33 

34try: 

35 newstr = unicode 

36except NameError: 

37 newstr = str 

38 

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

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

41 

42 

43encoding_gmt_to_python = { 

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

45 'standard+': 'ascii', 

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

47 'standard': 'ascii'} 

48 

49for i in range(1, 11): 

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

51 

52 

53def have_gmt(): 

54 try: 

55 get_gmt_installation('newest') 

56 return True 

57 

58 except GMTInstallationProblem: 

59 return False 

60 

61 

62def check_have_gmt(): 

63 if not have_gmt(): 

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

65 

66 

67def have_pixmaptools(): 

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

69 try: 

70 p = subprocess.Popen( 

71 prog, 

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

73 

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

75 

76 except OSError: 

77 return False 

78 

79 return True 

80 

81 

82class GmtPyError(Exception): 

83 pass 

84 

85 

86class GMTError(GmtPyError): 

87 pass 

88 

89 

90class GMTInstallationProblem(GmtPyError): 

91 pass 

92 

93 

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

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

96 

97 _, tmp_filename_base = tempfile.mkstemp() 

98 

99 try: 

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

101 fmt_arg = '-svg' 

102 tmp_filename = tmp_filename_base 

103 oversample = 1.0 

104 else: 

105 fmt_arg = '-png' 

106 tmp_filename = tmp_filename_base + '-1.png' 

107 

108 if size is not None: 

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

110 elif width is not None: 

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

112 elif height is not None: 

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

114 else: 

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

116 

117 try: 

118 subprocess.check_call( 

119 ['pdftocairo'] + scale_args + 

120 [fmt_arg, in_filename, tmp_filename_base]) 

121 except OSError as e: 

122 raise GmtPyError( 

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

124 

125 if oversample > 1.: 

126 try: 

127 subprocess.check_call([ 

128 'convert', 

129 tmp_filename, 

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

131 out_filename]) 

132 except OSError as e: 

133 raise GmtPyError( 

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

135 

136 else: 

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

138 shutil.move(tmp_filename, out_filename) 

139 else: 

140 try: 

141 subprocess.check_call( 

142 ['convert', tmp_filename, out_filename]) 

143 except Exception as e: 

144 raise GmtPyError( 

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

146 % str(e)) 

147 

148 except Exception: 

149 raise 

150 

151 finally: 

152 if os.path.exists(tmp_filename_base): 

153 os.remove(tmp_filename_base) 

154 

155 if os.path.exists(tmp_filename): 

156 os.remove(tmp_filename) 

157 

158 

159def get_bbox(s): 

160 for pat in [find_hiresbb, find_bb]: 

161 m = pat.search(s) 

162 if m: 

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

164 return bb 

165 

166 raise GmtPyError('Cannot find bbox') 

167 

168 

169def replace_bbox(bbox, *args): 

170 

171 def repl(m): 

172 if m.group(1): 

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

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

175 else: 

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

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

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

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

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

181 

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

183 if len(args) == 1: 

184 s = args[0] 

185 return pat.sub(repl, s) 

186 

187 else: 

188 fin, fout = args 

189 nn = 0 

190 for line in fin: 

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

192 nn += n 

193 fout.write(line) 

194 if nn == 2: 

195 break 

196 

197 if nn == 2: 

198 for line in fin: 

199 fout.write(line) 

200 

201 

202def escape_shell_arg(s): 

203 ''' 

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

205 insecure. 

206 ''' 

207 

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

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

210 else: 

211 return s 

212 

213 

214def escape_shell_args(args): 

215 ''' 

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

217 insecure. 

218 ''' 

219 

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

221 

222 

223golden_ratio = 1.61803 

224 

225# units in points 

226_units = { 

227 'i': 72., 

228 'c': 72./2.54, 

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

230 'p': 1.} 

231 

232inch = _units['i'] 

233cm = _units['c'] 

234 

235# some awsome colors 

236tango_colors = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

264} 

265 

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

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

268 'butter2')] 

269 

270 

271def color(x=None): 

272 ''' 

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

274 

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

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

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

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

279 transformed into the string form which GMT expects. 

280 ''' 

281 

282 if x is None: 

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

284 

285 if isinstance(x, int): 

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

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

288 else: 

289 return '0/0/0' 

290 

291 elif isinstance(x, str): 

292 if x in tango_colors: 

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

294 else: 

295 return x 

296 

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

298 

299 

300def color_tup(x=None): 

301 if x is None: 

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

303 

304 if isinstance(x, int): 

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

306 return graph_colors[x] 

307 else: 

308 return (0, 0, 0) 

309 

310 elif isinstance(x, str): 

311 if x in tango_colors: 

312 return tango_colors[x] 

313 

314 return x 

315 

316 

317_gmt_installations = {} 

318 

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

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

321 

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

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

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

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

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

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

328 

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

330 

331 

332def key_version(a): 

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

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

335 

336 

337def newest_installed_gmt_version(): 

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

339 

340 

341def all_installed_gmt_versions(): 

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

343 

344 

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

346# changed. 

347 

348_gmt_defaults_by_version = {} 

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

350# 

351# GMT-SYSTEM 4.2.1 Defaults file 

352# 

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

354PAGE_COLOR = 255/255/255 

355PAGE_ORIENTATION = portrait 

356PAPER_MEDIA = a4+ 

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

358ANNOT_MIN_ANGLE = 20 

359ANNOT_MIN_SPACING = 0 

360ANNOT_FONT_PRIMARY = Helvetica 

361ANNOT_FONT_SIZE = 12p 

362ANNOT_OFFSET_PRIMARY = 0.075i 

363ANNOT_FONT_SECONDARY = Helvetica 

364ANNOT_FONT_SIZE_SECONDARY = 16p 

365ANNOT_OFFSET_SECONDARY = 0.075i 

366DEGREE_SYMBOL = ring 

367HEADER_FONT = Helvetica 

368HEADER_FONT_SIZE = 36p 

369HEADER_OFFSET = 0.1875i 

370LABEL_FONT = Helvetica 

371LABEL_FONT_SIZE = 14p 

372LABEL_OFFSET = 0.1125i 

373OBLIQUE_ANNOTATION = 1 

374PLOT_CLOCK_FORMAT = hh:mm:ss 

375PLOT_DATE_FORMAT = yyyy-mm-dd 

376PLOT_DEGREE_FORMAT = +ddd:mm:ss 

377Y_AXIS_TYPE = hor_text 

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

379BASEMAP_AXES = WESN 

380BASEMAP_FRAME_RGB = 0/0/0 

381BASEMAP_TYPE = plain 

382FRAME_PEN = 1.25p 

383FRAME_WIDTH = 0.075i 

384GRID_CROSS_SIZE_PRIMARY = 0i 

385GRID_CROSS_SIZE_SECONDARY = 0i 

386GRID_PEN_PRIMARY = 0.25p 

387GRID_PEN_SECONDARY = 0.5p 

388MAP_SCALE_HEIGHT = 0.075i 

389TICK_LENGTH = 0.075i 

390POLAR_CAP = 85/90 

391TICK_PEN = 0.5p 

392X_AXIS_LENGTH = 9i 

393Y_AXIS_LENGTH = 6i 

394X_ORIGIN = 1i 

395Y_ORIGIN = 1i 

396UNIX_TIME = FALSE 

397UNIX_TIME_POS = -0.75i/-0.75i 

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

399COLOR_BACKGROUND = 0/0/0 

400COLOR_FOREGROUND = 255/255/255 

401COLOR_NAN = 128/128/128 

402COLOR_IMAGE = adobe 

403COLOR_MODEL = rgb 

404HSV_MIN_SATURATION = 1 

405HSV_MAX_SATURATION = 0.1 

406HSV_MIN_VALUE = 0.3 

407HSV_MAX_VALUE = 1 

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

409CHAR_ENCODING = ISOLatin1+ 

410DOTS_PR_INCH = 300 

411N_COPIES = 1 

412PS_COLOR = rgb 

413PS_IMAGE_COMPRESS = none 

414PS_IMAGE_FORMAT = ascii 

415PS_LINE_CAP = round 

416PS_LINE_JOIN = miter 

417PS_MITER_LIMIT = 35 

418PS_VERBOSE = FALSE 

419GLOBAL_X_SCALE = 1 

420GLOBAL_Y_SCALE = 1 

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

422D_FORMAT = %lg 

423FIELD_DELIMITER = tab 

424GRIDFILE_SHORTHAND = FALSE 

425GRID_FORMAT = nf 

426INPUT_CLOCK_FORMAT = hh:mm:ss 

427INPUT_DATE_FORMAT = yyyy-mm-dd 

428IO_HEADER = FALSE 

429N_HEADER_RECS = 1 

430OUTPUT_CLOCK_FORMAT = hh:mm:ss 

431OUTPUT_DATE_FORMAT = yyyy-mm-dd 

432OUTPUT_DEGREE_FORMAT = +D 

433XY_TOGGLE = FALSE 

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

435ELLIPSOID = WGS-84 

436MAP_SCALE_FACTOR = default 

437MEASURE_UNIT = inch 

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

439TIME_FORMAT_PRIMARY = full 

440TIME_FORMAT_SECONDARY = full 

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

442TIME_IS_INTERVAL = OFF 

443TIME_INTERVAL_FRACTION = 0.5 

444TIME_LANGUAGE = us 

445TIME_SYSTEM = other 

446TIME_UNIT = d 

447TIME_WEEK_START = Sunday 

448Y2K_OFFSET_YEAR = 1950 

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

450HISTORY = TRUE 

451INTERPOLANT = akima 

452LINE_STEP = 0.01i 

453VECTOR_SHAPE = 0 

454VERBOSE = FALSE''' 

455 

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

457# 

458# GMT-SYSTEM 4.3.0 Defaults file 

459# 

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

461PAGE_COLOR = 255/255/255 

462PAGE_ORIENTATION = portrait 

463PAPER_MEDIA = a4+ 

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

465ANNOT_MIN_ANGLE = 20 

466ANNOT_MIN_SPACING = 0 

467ANNOT_FONT_PRIMARY = Helvetica 

468ANNOT_FONT_SIZE_PRIMARY = 12p 

469ANNOT_OFFSET_PRIMARY = 0.075i 

470ANNOT_FONT_SECONDARY = Helvetica 

471ANNOT_FONT_SIZE_SECONDARY = 16p 

472ANNOT_OFFSET_SECONDARY = 0.075i 

473DEGREE_SYMBOL = ring 

474HEADER_FONT = Helvetica 

475HEADER_FONT_SIZE = 36p 

476HEADER_OFFSET = 0.1875i 

477LABEL_FONT = Helvetica 

478LABEL_FONT_SIZE = 14p 

479LABEL_OFFSET = 0.1125i 

480OBLIQUE_ANNOTATION = 1 

481PLOT_CLOCK_FORMAT = hh:mm:ss 

482PLOT_DATE_FORMAT = yyyy-mm-dd 

483PLOT_DEGREE_FORMAT = +ddd:mm:ss 

484Y_AXIS_TYPE = hor_text 

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

486BASEMAP_AXES = WESN 

487BASEMAP_FRAME_RGB = 0/0/0 

488BASEMAP_TYPE = plain 

489FRAME_PEN = 1.25p 

490FRAME_WIDTH = 0.075i 

491GRID_CROSS_SIZE_PRIMARY = 0i 

492GRID_PEN_PRIMARY = 0.25p 

493GRID_CROSS_SIZE_SECONDARY = 0i 

494GRID_PEN_SECONDARY = 0.5p 

495MAP_SCALE_HEIGHT = 0.075i 

496POLAR_CAP = 85/90 

497TICK_LENGTH = 0.075i 

498TICK_PEN = 0.5p 

499X_AXIS_LENGTH = 9i 

500Y_AXIS_LENGTH = 6i 

501X_ORIGIN = 1i 

502Y_ORIGIN = 1i 

503UNIX_TIME = FALSE 

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

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

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

507COLOR_BACKGROUND = 0/0/0 

508COLOR_FOREGROUND = 255/255/255 

509COLOR_NAN = 128/128/128 

510COLOR_IMAGE = adobe 

511COLOR_MODEL = rgb 

512HSV_MIN_SATURATION = 1 

513HSV_MAX_SATURATION = 0.1 

514HSV_MIN_VALUE = 0.3 

515HSV_MAX_VALUE = 1 

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

517CHAR_ENCODING = ISOLatin1+ 

518DOTS_PR_INCH = 300 

519N_COPIES = 1 

520PS_COLOR = rgb 

521PS_IMAGE_COMPRESS = none 

522PS_IMAGE_FORMAT = ascii 

523PS_LINE_CAP = round 

524PS_LINE_JOIN = miter 

525PS_MITER_LIMIT = 35 

526PS_VERBOSE = FALSE 

527GLOBAL_X_SCALE = 1 

528GLOBAL_Y_SCALE = 1 

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

530D_FORMAT = %lg 

531FIELD_DELIMITER = tab 

532GRIDFILE_SHORTHAND = FALSE 

533GRID_FORMAT = nf 

534INPUT_CLOCK_FORMAT = hh:mm:ss 

535INPUT_DATE_FORMAT = yyyy-mm-dd 

536IO_HEADER = FALSE 

537N_HEADER_RECS = 1 

538OUTPUT_CLOCK_FORMAT = hh:mm:ss 

539OUTPUT_DATE_FORMAT = yyyy-mm-dd 

540OUTPUT_DEGREE_FORMAT = +D 

541XY_TOGGLE = FALSE 

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

543ELLIPSOID = WGS-84 

544MAP_SCALE_FACTOR = default 

545MEASURE_UNIT = inch 

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

547TIME_FORMAT_PRIMARY = full 

548TIME_FORMAT_SECONDARY = full 

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

550TIME_IS_INTERVAL = OFF 

551TIME_INTERVAL_FRACTION = 0.5 

552TIME_LANGUAGE = us 

553TIME_UNIT = d 

554TIME_WEEK_START = Sunday 

555Y2K_OFFSET_YEAR = 1950 

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

557HISTORY = TRUE 

558INTERPOLANT = akima 

559LINE_STEP = 0.01i 

560VECTOR_SHAPE = 0 

561VERBOSE = FALSE''' 

562 

563 

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

565# 

566# GMT-SYSTEM 4.3.1 Defaults file 

567# 

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

569PAGE_COLOR = 255/255/255 

570PAGE_ORIENTATION = portrait 

571PAPER_MEDIA = a4+ 

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

573ANNOT_MIN_ANGLE = 20 

574ANNOT_MIN_SPACING = 0 

575ANNOT_FONT_PRIMARY = Helvetica 

576ANNOT_FONT_SIZE_PRIMARY = 12p 

577ANNOT_OFFSET_PRIMARY = 0.075i 

578ANNOT_FONT_SECONDARY = Helvetica 

579ANNOT_FONT_SIZE_SECONDARY = 16p 

580ANNOT_OFFSET_SECONDARY = 0.075i 

581DEGREE_SYMBOL = ring 

582HEADER_FONT = Helvetica 

583HEADER_FONT_SIZE = 36p 

584HEADER_OFFSET = 0.1875i 

585LABEL_FONT = Helvetica 

586LABEL_FONT_SIZE = 14p 

587LABEL_OFFSET = 0.1125i 

588OBLIQUE_ANNOTATION = 1 

589PLOT_CLOCK_FORMAT = hh:mm:ss 

590PLOT_DATE_FORMAT = yyyy-mm-dd 

591PLOT_DEGREE_FORMAT = +ddd:mm:ss 

592Y_AXIS_TYPE = hor_text 

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

594BASEMAP_AXES = WESN 

595BASEMAP_FRAME_RGB = 0/0/0 

596BASEMAP_TYPE = plain 

597FRAME_PEN = 1.25p 

598FRAME_WIDTH = 0.075i 

599GRID_CROSS_SIZE_PRIMARY = 0i 

600GRID_PEN_PRIMARY = 0.25p 

601GRID_CROSS_SIZE_SECONDARY = 0i 

602GRID_PEN_SECONDARY = 0.5p 

603MAP_SCALE_HEIGHT = 0.075i 

604POLAR_CAP = 85/90 

605TICK_LENGTH = 0.075i 

606TICK_PEN = 0.5p 

607X_AXIS_LENGTH = 9i 

608Y_AXIS_LENGTH = 6i 

609X_ORIGIN = 1i 

610Y_ORIGIN = 1i 

611UNIX_TIME = FALSE 

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

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

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

615COLOR_BACKGROUND = 0/0/0 

616COLOR_FOREGROUND = 255/255/255 

617COLOR_NAN = 128/128/128 

618COLOR_IMAGE = adobe 

619COLOR_MODEL = rgb 

620HSV_MIN_SATURATION = 1 

621HSV_MAX_SATURATION = 0.1 

622HSV_MIN_VALUE = 0.3 

623HSV_MAX_VALUE = 1 

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

625CHAR_ENCODING = ISOLatin1+ 

626DOTS_PR_INCH = 300 

627N_COPIES = 1 

628PS_COLOR = rgb 

629PS_IMAGE_COMPRESS = none 

630PS_IMAGE_FORMAT = ascii 

631PS_LINE_CAP = round 

632PS_LINE_JOIN = miter 

633PS_MITER_LIMIT = 35 

634PS_VERBOSE = FALSE 

635GLOBAL_X_SCALE = 1 

636GLOBAL_Y_SCALE = 1 

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

638D_FORMAT = %lg 

639FIELD_DELIMITER = tab 

640GRIDFILE_SHORTHAND = FALSE 

641GRID_FORMAT = nf 

642INPUT_CLOCK_FORMAT = hh:mm:ss 

643INPUT_DATE_FORMAT = yyyy-mm-dd 

644IO_HEADER = FALSE 

645N_HEADER_RECS = 1 

646OUTPUT_CLOCK_FORMAT = hh:mm:ss 

647OUTPUT_DATE_FORMAT = yyyy-mm-dd 

648OUTPUT_DEGREE_FORMAT = +D 

649XY_TOGGLE = FALSE 

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

651ELLIPSOID = WGS-84 

652MAP_SCALE_FACTOR = default 

653MEASURE_UNIT = inch 

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

655TIME_FORMAT_PRIMARY = full 

656TIME_FORMAT_SECONDARY = full 

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

658TIME_IS_INTERVAL = OFF 

659TIME_INTERVAL_FRACTION = 0.5 

660TIME_LANGUAGE = us 

661TIME_UNIT = d 

662TIME_WEEK_START = Sunday 

663Y2K_OFFSET_YEAR = 1950 

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

665HISTORY = TRUE 

666INTERPOLANT = akima 

667LINE_STEP = 0.01i 

668VECTOR_SHAPE = 0 

669VERBOSE = FALSE''' 

670 

671 

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

673# 

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

675# 

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

677PAGE_COLOR = 255/255/255 

678PAGE_ORIENTATION = portrait 

679PAPER_MEDIA = a4+ 

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

681ANNOT_MIN_ANGLE = 20 

682ANNOT_MIN_SPACING = 0 

683ANNOT_FONT_PRIMARY = Helvetica 

684ANNOT_FONT_SIZE_PRIMARY = 14p 

685ANNOT_OFFSET_PRIMARY = 0.075i 

686ANNOT_FONT_SECONDARY = Helvetica 

687ANNOT_FONT_SIZE_SECONDARY = 16p 

688ANNOT_OFFSET_SECONDARY = 0.075i 

689DEGREE_SYMBOL = ring 

690HEADER_FONT = Helvetica 

691HEADER_FONT_SIZE = 36p 

692HEADER_OFFSET = 0.1875i 

693LABEL_FONT = Helvetica 

694LABEL_FONT_SIZE = 14p 

695LABEL_OFFSET = 0.1125i 

696OBLIQUE_ANNOTATION = 1 

697PLOT_CLOCK_FORMAT = hh:mm:ss 

698PLOT_DATE_FORMAT = yyyy-mm-dd 

699PLOT_DEGREE_FORMAT = +ddd:mm:ss 

700Y_AXIS_TYPE = hor_text 

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

702BASEMAP_AXES = WESN 

703BASEMAP_FRAME_RGB = 0/0/0 

704BASEMAP_TYPE = plain 

705FRAME_PEN = 1.25p 

706FRAME_WIDTH = 0.075i 

707GRID_CROSS_SIZE_PRIMARY = 0i 

708GRID_PEN_PRIMARY = 0.25p 

709GRID_CROSS_SIZE_SECONDARY = 0i 

710GRID_PEN_SECONDARY = 0.5p 

711MAP_SCALE_HEIGHT = 0.075i 

712POLAR_CAP = 85/90 

713TICK_LENGTH = 0.075i 

714TICK_PEN = 0.5p 

715X_AXIS_LENGTH = 9i 

716Y_AXIS_LENGTH = 6i 

717X_ORIGIN = 1i 

718Y_ORIGIN = 1i 

719UNIX_TIME = FALSE 

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

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

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

723COLOR_BACKGROUND = 0/0/0 

724COLOR_FOREGROUND = 255/255/255 

725COLOR_NAN = 128/128/128 

726COLOR_IMAGE = adobe 

727COLOR_MODEL = rgb 

728HSV_MIN_SATURATION = 1 

729HSV_MAX_SATURATION = 0.1 

730HSV_MIN_VALUE = 0.3 

731HSV_MAX_VALUE = 1 

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

733CHAR_ENCODING = ISOLatin1+ 

734DOTS_PR_INCH = 300 

735N_COPIES = 1 

736PS_COLOR = rgb 

737PS_IMAGE_COMPRESS = lzw 

738PS_IMAGE_FORMAT = ascii 

739PS_LINE_CAP = round 

740PS_LINE_JOIN = miter 

741PS_MITER_LIMIT = 35 

742PS_VERBOSE = FALSE 

743GLOBAL_X_SCALE = 1 

744GLOBAL_Y_SCALE = 1 

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

746D_FORMAT = %lg 

747FIELD_DELIMITER = tab 

748GRIDFILE_SHORTHAND = FALSE 

749GRID_FORMAT = nf 

750INPUT_CLOCK_FORMAT = hh:mm:ss 

751INPUT_DATE_FORMAT = yyyy-mm-dd 

752IO_HEADER = FALSE 

753N_HEADER_RECS = 1 

754OUTPUT_CLOCK_FORMAT = hh:mm:ss 

755OUTPUT_DATE_FORMAT = yyyy-mm-dd 

756OUTPUT_DEGREE_FORMAT = +D 

757XY_TOGGLE = FALSE 

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

759ELLIPSOID = WGS-84 

760MAP_SCALE_FACTOR = default 

761MEASURE_UNIT = inch 

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

763TIME_FORMAT_PRIMARY = full 

764TIME_FORMAT_SECONDARY = full 

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

766TIME_IS_INTERVAL = OFF 

767TIME_INTERVAL_FRACTION = 0.5 

768TIME_LANGUAGE = us 

769TIME_UNIT = d 

770TIME_WEEK_START = Sunday 

771Y2K_OFFSET_YEAR = 1950 

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

773HISTORY = TRUE 

774INTERPOLANT = akima 

775LINE_STEP = 0.01i 

776VECTOR_SHAPE = 0 

777VERBOSE = FALSE 

778''' 

779 

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

781# 

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

783# 

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

785PAGE_COLOR = white 

786PAGE_ORIENTATION = portrait 

787PAPER_MEDIA = a4+ 

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

789ANNOT_MIN_ANGLE = 20 

790ANNOT_MIN_SPACING = 0 

791ANNOT_FONT_PRIMARY = Helvetica 

792ANNOT_FONT_SIZE_PRIMARY = 14p 

793ANNOT_OFFSET_PRIMARY = 0.075i 

794ANNOT_FONT_SECONDARY = Helvetica 

795ANNOT_FONT_SIZE_SECONDARY = 16p 

796ANNOT_OFFSET_SECONDARY = 0.075i 

797DEGREE_SYMBOL = ring 

798HEADER_FONT = Helvetica 

799HEADER_FONT_SIZE = 36p 

800HEADER_OFFSET = 0.1875i 

801LABEL_FONT = Helvetica 

802LABEL_FONT_SIZE = 14p 

803LABEL_OFFSET = 0.1125i 

804OBLIQUE_ANNOTATION = 1 

805PLOT_CLOCK_FORMAT = hh:mm:ss 

806PLOT_DATE_FORMAT = yyyy-mm-dd 

807PLOT_DEGREE_FORMAT = +ddd:mm:ss 

808Y_AXIS_TYPE = hor_text 

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

810BASEMAP_AXES = WESN 

811BASEMAP_FRAME_RGB = black 

812BASEMAP_TYPE = plain 

813FRAME_PEN = 1.25p 

814FRAME_WIDTH = 0.075i 

815GRID_CROSS_SIZE_PRIMARY = 0i 

816GRID_PEN_PRIMARY = 0.25p 

817GRID_CROSS_SIZE_SECONDARY = 0i 

818GRID_PEN_SECONDARY = 0.5p 

819MAP_SCALE_HEIGHT = 0.075i 

820POLAR_CAP = 85/90 

821TICK_LENGTH = 0.075i 

822TICK_PEN = 0.5p 

823X_AXIS_LENGTH = 9i 

824Y_AXIS_LENGTH = 6i 

825X_ORIGIN = 1i 

826Y_ORIGIN = 1i 

827UNIX_TIME = FALSE 

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

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

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

831COLOR_BACKGROUND = black 

832COLOR_FOREGROUND = white 

833COLOR_NAN = 128 

834COLOR_IMAGE = adobe 

835COLOR_MODEL = rgb 

836HSV_MIN_SATURATION = 1 

837HSV_MAX_SATURATION = 0.1 

838HSV_MIN_VALUE = 0.3 

839HSV_MAX_VALUE = 1 

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

841CHAR_ENCODING = ISOLatin1+ 

842DOTS_PR_INCH = 300 

843GLOBAL_X_SCALE = 1 

844GLOBAL_Y_SCALE = 1 

845N_COPIES = 1 

846PS_COLOR = rgb 

847PS_IMAGE_COMPRESS = lzw 

848PS_IMAGE_FORMAT = ascii 

849PS_LINE_CAP = round 

850PS_LINE_JOIN = miter 

851PS_MITER_LIMIT = 35 

852PS_VERBOSE = FALSE 

853TRANSPARENCY = 0 

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

855D_FORMAT = %.12lg 

856FIELD_DELIMITER = tab 

857GRIDFILE_FORMAT = nf 

858GRIDFILE_SHORTHAND = FALSE 

859INPUT_CLOCK_FORMAT = hh:mm:ss 

860INPUT_DATE_FORMAT = yyyy-mm-dd 

861IO_HEADER = FALSE 

862N_HEADER_RECS = 1 

863NAN_RECORDS = pass 

864OUTPUT_CLOCK_FORMAT = hh:mm:ss 

865OUTPUT_DATE_FORMAT = yyyy-mm-dd 

866OUTPUT_DEGREE_FORMAT = D 

867XY_TOGGLE = FALSE 

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

869ELLIPSOID = WGS-84 

870MAP_SCALE_FACTOR = default 

871MEASURE_UNIT = inch 

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

873TIME_FORMAT_PRIMARY = full 

874TIME_FORMAT_SECONDARY = full 

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

876TIME_IS_INTERVAL = OFF 

877TIME_INTERVAL_FRACTION = 0.5 

878TIME_LANGUAGE = us 

879TIME_UNIT = d 

880TIME_WEEK_START = Sunday 

881Y2K_OFFSET_YEAR = 1950 

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

883HISTORY = TRUE 

884INTERPOLANT = akima 

885LINE_STEP = 0.01i 

886VECTOR_SHAPE = 0 

887VERBOSE = FALSE 

888''' 

889 

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

891# 

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

893# 

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

895PAGE_COLOR = white 

896PAGE_ORIENTATION = portrait 

897PAPER_MEDIA = a4+ 

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

899ANNOT_MIN_ANGLE = 20 

900ANNOT_MIN_SPACING = 0 

901ANNOT_FONT_PRIMARY = Helvetica 

902ANNOT_FONT_SIZE_PRIMARY = 14p 

903ANNOT_OFFSET_PRIMARY = 0.075i 

904ANNOT_FONT_SECONDARY = Helvetica 

905ANNOT_FONT_SIZE_SECONDARY = 16p 

906ANNOT_OFFSET_SECONDARY = 0.075i 

907DEGREE_SYMBOL = ring 

908HEADER_FONT = Helvetica 

909HEADER_FONT_SIZE = 36p 

910HEADER_OFFSET = 0.1875i 

911LABEL_FONT = Helvetica 

912LABEL_FONT_SIZE = 14p 

913LABEL_OFFSET = 0.1125i 

914OBLIQUE_ANNOTATION = 1 

915PLOT_CLOCK_FORMAT = hh:mm:ss 

916PLOT_DATE_FORMAT = yyyy-mm-dd 

917PLOT_DEGREE_FORMAT = +ddd:mm:ss 

918Y_AXIS_TYPE = hor_text 

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

920BASEMAP_AXES = WESN 

921BASEMAP_FRAME_RGB = black 

922BASEMAP_TYPE = plain 

923FRAME_PEN = 1.25p 

924FRAME_WIDTH = 0.075i 

925GRID_CROSS_SIZE_PRIMARY = 0i 

926GRID_PEN_PRIMARY = 0.25p 

927GRID_CROSS_SIZE_SECONDARY = 0i 

928GRID_PEN_SECONDARY = 0.5p 

929MAP_SCALE_HEIGHT = 0.075i 

930POLAR_CAP = 85/90 

931TICK_LENGTH = 0.075i 

932TICK_PEN = 0.5p 

933X_AXIS_LENGTH = 9i 

934Y_AXIS_LENGTH = 6i 

935X_ORIGIN = 1i 

936Y_ORIGIN = 1i 

937UNIX_TIME = FALSE 

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

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

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

941COLOR_BACKGROUND = black 

942COLOR_FOREGROUND = white 

943COLOR_NAN = 128 

944COLOR_IMAGE = adobe 

945COLOR_MODEL = rgb 

946HSV_MIN_SATURATION = 1 

947HSV_MAX_SATURATION = 0.1 

948HSV_MIN_VALUE = 0.3 

949HSV_MAX_VALUE = 1 

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

951CHAR_ENCODING = ISOLatin1+ 

952DOTS_PR_INCH = 300 

953GLOBAL_X_SCALE = 1 

954GLOBAL_Y_SCALE = 1 

955N_COPIES = 1 

956PS_COLOR = rgb 

957PS_IMAGE_COMPRESS = lzw 

958PS_IMAGE_FORMAT = ascii 

959PS_LINE_CAP = round 

960PS_LINE_JOIN = miter 

961PS_MITER_LIMIT = 35 

962PS_VERBOSE = FALSE 

963TRANSPARENCY = 0 

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

965D_FORMAT = %.12lg 

966FIELD_DELIMITER = tab 

967GRIDFILE_FORMAT = nf 

968GRIDFILE_SHORTHAND = FALSE 

969INPUT_CLOCK_FORMAT = hh:mm:ss 

970INPUT_DATE_FORMAT = yyyy-mm-dd 

971IO_HEADER = FALSE 

972N_HEADER_RECS = 1 

973NAN_RECORDS = pass 

974OUTPUT_CLOCK_FORMAT = hh:mm:ss 

975OUTPUT_DATE_FORMAT = yyyy-mm-dd 

976OUTPUT_DEGREE_FORMAT = D 

977XY_TOGGLE = FALSE 

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

979ELLIPSOID = WGS-84 

980MAP_SCALE_FACTOR = default 

981MEASURE_UNIT = inch 

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

983TIME_FORMAT_PRIMARY = full 

984TIME_FORMAT_SECONDARY = full 

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

986TIME_IS_INTERVAL = OFF 

987TIME_INTERVAL_FRACTION = 0.5 

988TIME_LANGUAGE = us 

989TIME_UNIT = d 

990TIME_WEEK_START = Sunday 

991Y2K_OFFSET_YEAR = 1950 

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

993HISTORY = TRUE 

994INTERPOLANT = akima 

995LINE_STEP = 0.01i 

996VECTOR_SHAPE = 0 

997VERBOSE = FALSE 

998''' 

999 

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

1001# 

1002# GMT 5.1.2 Defaults file 

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

1004# $Revision: 13836 $ 

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

1006# 

1007# COLOR Parameters 

1008# 

1009COLOR_BACKGROUND = black 

1010COLOR_FOREGROUND = white 

1011COLOR_NAN = 127.5 

1012COLOR_MODEL = none 

1013COLOR_HSV_MIN_S = 1 

1014COLOR_HSV_MAX_S = 0.1 

1015COLOR_HSV_MIN_V = 0.3 

1016COLOR_HSV_MAX_V = 1 

1017# 

1018# DIR Parameters 

1019# 

1020DIR_DATA = 

1021DIR_DCW = 

1022DIR_GSHHG = 

1023# 

1024# FONT Parameters 

1025# 

1026FONT_ANNOT_PRIMARY = 14p,Helvetica,black 

1027FONT_ANNOT_SECONDARY = 16p,Helvetica,black 

1028FONT_LABEL = 14p,Helvetica,black 

1029FONT_LOGO = 8p,Helvetica,black 

1030FONT_TITLE = 24p,Helvetica,black 

1031# 

1032# FORMAT Parameters 

1033# 

1034FORMAT_CLOCK_IN = hh:mm:ss 

1035FORMAT_CLOCK_OUT = hh:mm:ss 

1036FORMAT_CLOCK_MAP = hh:mm:ss 

1037FORMAT_DATE_IN = yyyy-mm-dd 

1038FORMAT_DATE_OUT = yyyy-mm-dd 

1039FORMAT_DATE_MAP = yyyy-mm-dd 

1040FORMAT_GEO_OUT = D 

1041FORMAT_GEO_MAP = ddd:mm:ss 

1042FORMAT_FLOAT_OUT = %.12g 

1043FORMAT_FLOAT_MAP = %.12g 

1044FORMAT_TIME_PRIMARY_MAP = full 

1045FORMAT_TIME_SECONDARY_MAP = full 

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

1047# 

1048# GMT Miscellaneous Parameters 

1049# 

1050GMT_COMPATIBILITY = 4 

1051GMT_CUSTOM_LIBS = 

1052GMT_EXTRAPOLATE_VAL = NaN 

1053GMT_FFT = auto 

1054GMT_HISTORY = true 

1055GMT_INTERPOLANT = akima 

1056GMT_TRIANGULATE = Shewchuk 

1057GMT_VERBOSE = compat 

1058GMT_LANGUAGE = us 

1059# 

1060# I/O Parameters 

1061# 

1062IO_COL_SEPARATOR = tab 

1063IO_GRIDFILE_FORMAT = nf 

1064IO_GRIDFILE_SHORTHAND = false 

1065IO_HEADER = false 

1066IO_N_HEADER_RECS = 0 

1067IO_NAN_RECORDS = pass 

1068IO_NC4_CHUNK_SIZE = auto 

1069IO_NC4_DEFLATION_LEVEL = 3 

1070IO_LONLAT_TOGGLE = false 

1071IO_SEGMENT_MARKER = > 

1072# 

1073# MAP Parameters 

1074# 

1075MAP_ANNOT_MIN_ANGLE = 20 

1076MAP_ANNOT_MIN_SPACING = 0p 

1077MAP_ANNOT_OBLIQUE = 1 

1078MAP_ANNOT_OFFSET_PRIMARY = 0.075i 

1079MAP_ANNOT_OFFSET_SECONDARY = 0.075i 

1080MAP_ANNOT_ORTHO = we 

1081MAP_DEFAULT_PEN = default,black 

1082MAP_DEGREE_SYMBOL = ring 

1083MAP_FRAME_AXES = WESNZ 

1084MAP_FRAME_PEN = thicker,black 

1085MAP_FRAME_TYPE = fancy 

1086MAP_FRAME_WIDTH = 5p 

1087MAP_GRID_CROSS_SIZE_PRIMARY = 0p 

1088MAP_GRID_CROSS_SIZE_SECONDARY = 0p 

1089MAP_GRID_PEN_PRIMARY = default,black 

1090MAP_GRID_PEN_SECONDARY = thinner,black 

1091MAP_LABEL_OFFSET = 0.1944i 

1092MAP_LINE_STEP = 0.75p 

1093MAP_LOGO = false 

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

1095MAP_ORIGIN_X = 1i 

1096MAP_ORIGIN_Y = 1i 

1097MAP_POLAR_CAP = 85/90 

1098MAP_SCALE_HEIGHT = 5p 

1099MAP_TICK_LENGTH_PRIMARY = 5p/2.5p 

1100MAP_TICK_LENGTH_SECONDARY = 15p/3.75p 

1101MAP_TICK_PEN_PRIMARY = thinner,black 

1102MAP_TICK_PEN_SECONDARY = thinner,black 

1103MAP_TITLE_OFFSET = 14p 

1104MAP_VECTOR_SHAPE = 0 

1105# 

1106# Projection Parameters 

1107# 

1108PROJ_AUX_LATITUDE = authalic 

1109PROJ_ELLIPSOID = WGS-84 

1110PROJ_LENGTH_UNIT = cm 

1111PROJ_MEAN_RADIUS = authalic 

1112PROJ_SCALE_FACTOR = default 

1113# 

1114# PostScript Parameters 

1115# 

1116PS_CHAR_ENCODING = ISOLatin1+ 

1117PS_COLOR_MODEL = rgb 

1118PS_COMMENTS = false 

1119PS_IMAGE_COMPRESS = deflate,5 

1120PS_LINE_CAP = butt 

1121PS_LINE_JOIN = miter 

1122PS_MITER_LIMIT = 35 

1123PS_MEDIA = a4 

1124PS_PAGE_COLOR = white 

1125PS_PAGE_ORIENTATION = portrait 

1126PS_SCALE_X = 1 

1127PS_SCALE_Y = 1 

1128PS_TRANSPARENCY = Normal 

1129# 

1130# Calendar/Time Parameters 

1131# 

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

1133TIME_IS_INTERVAL = off 

1134TIME_INTERVAL_FRACTION = 0.5 

1135TIME_UNIT = s 

1136TIME_WEEK_START = Monday 

1137TIME_Y2K_OFFSET_YEAR = 1950 

1138''' 

1139 

1140 

1141def get_gmt_version(gmtdefaultsbinary, gmthomedir=None): 

1142 args = [gmtdefaultsbinary] 

1143 

1144 environ = os.environ.copy() 

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

1146 

1147 p = subprocess.Popen( 

1148 args, 

1149 stdout=subprocess.PIPE, 

1150 stderr=subprocess.PIPE, 

1151 env=environ) 

1152 

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

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

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

1156 

1157 if not m: 

1158 raise GMTInstallationProblem( 

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

1160 % gmtdefaultsbinary) 

1161 

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

1163 

1164 

1165def detect_gmt_installations(): 

1166 

1167 installations = {} 

1168 errmesses = [] 

1169 

1170 # GMT 4.x: 

1171 try: 

1172 p = subprocess.Popen( 

1173 ['GMT'], 

1174 stdout=subprocess.PIPE, 

1175 stderr=subprocess.PIPE) 

1176 

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

1178 

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

1180 if not m: 

1181 raise GMTInstallationProblem( 

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

1183 

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

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

1186 

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

1188 if not m: 

1189 raise GMTInstallationProblem( 

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

1191 

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

1193 

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

1195 if not m: 

1196 raise GMTInstallationProblem( 

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

1198 

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

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

1201 raise GMTInstallationProblem( 

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

1203 

1204 gmthome = gmtshare[:-6] 

1205 

1206 installations[version] = { 

1207 'home': gmthome, 

1208 'bin': gmtbin} 

1209 

1210 except OSError as e: 

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

1212 

1213 try: 

1214 version = str(subprocess.check_output( 

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

1216 gmtbin = str(subprocess.check_output( 

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

1218 installations[version] = { 

1219 'bin': gmtbin} 

1220 

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

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

1223 

1224 if not installations: 

1225 s = [] 

1226 for (progname, errmess) in errmesses: 

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

1228 

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

1230 

1231 return installations 

1232 

1233 

1234def appropriate_defaults_version(version): 

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

1236 for iavail, avail in enumerate(avails): 

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

1238 return version 

1239 

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

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

1242 

1243 return avails[-1] 

1244 

1245 

1246def gmt_default_config(version): 

1247 ''' 

1248 Get default GMT configuration dict for given version. 

1249 ''' 

1250 

1251 xversion = appropriate_defaults_version(version) 

1252 

1253 # if not version in _gmt_defaults_by_version: 

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

1255 

1256 gmt_defaults = _gmt_defaults_by_version[xversion] 

1257 

1258 d = {} 

1259 for line in gmt_defaults.splitlines(): 

1260 sline = line.strip() 

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

1262 continue 

1263 

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

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

1266 

1267 return d 

1268 

1269 

1270def diff_defaults(v1, v2): 

1271 d1 = gmt_default_config(v1) 

1272 d2 = gmt_default_config(v2) 

1273 for k in d1: 

1274 if k not in d2: 

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

1276 else: 

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

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

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

1280 

1281 for k in d2: 

1282 if k not in d1: 

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

1284 

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

1286 

1287 

1288def check_gmt_installation(installation): 

1289 

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

1291 bin_dir = installation['bin'] 

1292 version = installation['version'] 

1293 

1294 for d in home_dir, bin_dir: 

1295 if d is not None: 

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

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

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

1299 

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

1301 

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

1303 gmtdefaults = pjoin(bin_dir, 'gmtdefaults') 

1304 

1305 versionfound = get_gmt_version(gmtdefaults, home_dir) 

1306 

1307 if versionfound != version: 

1308 raise GMTInstallationProblem(( 

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

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

1311 version, versionfound, gmtdefaults)) 

1312 

1313 

1314def get_gmt_installation(version): 

1315 setup_gmt_installations() 

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

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

1318 % (version, newest_installed_gmt_version())) 

1319 

1320 version = 'newest' 

1321 

1322 if version == 'newest': 

1323 version = newest_installed_gmt_version() 

1324 

1325 installation = dict(_gmt_installations[version]) 

1326 

1327 return installation 

1328 

1329 

1330def setup_gmt_installations(): 

1331 if not setup_gmt_installations.have_done: 

1332 if not _gmt_installations: 

1333 

1334 _gmt_installations.update(detect_gmt_installations()) 

1335 

1336 # store defaults as dicts into the gmt installations dicts 

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

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

1339 installation['version'] = version 

1340 

1341 for installation in _gmt_installations.values(): 

1342 check_gmt_installation(installation) 

1343 

1344 setup_gmt_installations.have_done = True 

1345 

1346 

1347setup_gmt_installations.have_done = False 

1348 

1349_paper_sizes_a = '''A0 2380 3368 

1350 A1 1684 2380 

1351 A2 1190 1684 

1352 A3 842 1190 

1353 A4 595 842 

1354 A5 421 595 

1355 A6 297 421 

1356 A7 210 297 

1357 A8 148 210 

1358 A9 105 148 

1359 A10 74 105 

1360 B0 2836 4008 

1361 B1 2004 2836 

1362 B2 1418 2004 

1363 B3 1002 1418 

1364 B4 709 1002 

1365 B5 501 709 

1366 archA 648 864 

1367 archB 864 1296 

1368 archC 1296 1728 

1369 archD 1728 2592 

1370 archE 2592 3456 

1371 flsa 612 936 

1372 halfletter 396 612 

1373 note 540 720 

1374 letter 612 792 

1375 legal 612 1008 

1376 11x17 792 1224 

1377 ledger 1224 792''' 

1378 

1379 

1380_paper_sizes = {} 

1381 

1382 

1383def setup_paper_sizes(): 

1384 if not _paper_sizes: 

1385 for line in _paper_sizes_a.splitlines(): 

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

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

1388 

1389 

1390def get_paper_size(k): 

1391 setup_paper_sizes() 

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

1393 

1394 

1395def all_paper_sizes(): 

1396 setup_paper_sizes() 

1397 return _paper_sizes 

1398 

1399 

1400def measure_unit(gmt_config): 

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

1402 if k in gmt_config: 

1403 return gmt_config[k] 

1404 

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

1406 

1407 

1408def paper_media(gmt_config): 

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

1410 if k in gmt_config: 

1411 return gmt_config[k] 

1412 

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

1414 

1415 

1416def page_orientation(gmt_config): 

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

1418 if k in gmt_config: 

1419 return gmt_config[k] 

1420 

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

1422 

1423 

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

1425 

1426 leftmargin, topmargin, rightmargin, bottommargin = margins 

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

1428 

1429 paper_size = get_paper_size(paper_media(gmt_config)) 

1430 if not portrait: 

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

1432 

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

1434 2.0 + leftmargin 

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

1436 2.0 + bottommargin 

1437 

1438 if portrait: 

1439 bb1 = int((xoffset - leftmargin)) 

1440 bb2 = int((yoffset - bottommargin)) 

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

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

1443 else: 

1444 bb1 = int((yoffset - topmargin)) 

1445 bb2 = int((xoffset - leftmargin)) 

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

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

1448 

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

1450 

1451 

1452def gmtdefaults_as_text(version='newest'): 

1453 

1454 ''' 

1455 Get the built-in gmtdefaults. 

1456 ''' 

1457 

1458 if version not in _gmt_installations: 

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

1460 % (version, newest_installed_gmt_version())) 

1461 version = 'newest' 

1462 

1463 if version == 'newest': 

1464 version = newest_installed_gmt_version() 

1465 

1466 return _gmt_defaults_by_version[version] 

1467 

1468 

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

1470 ''' 

1471 Write COARDS compliant netcdf (grd) file. 

1472 ''' 

1473 

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

1475 ny, nx = z.shape 

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

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

1478 

1479 if naming == 'xy': 

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

1481 else: 

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

1483 

1484 nc.node_offset = 0 

1485 if title is not None: 

1486 nc.title = title 

1487 

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

1489 nc.createDimension(kx, nx) 

1490 nc.createDimension(ky, ny) 

1491 

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

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

1494 if naming == 'xy': 

1495 xvar.long_name = kx 

1496 yvar.long_name = ky 

1497 else: 

1498 xvar.long_name = 'longitude' 

1499 xvar.units = 'degrees_east' 

1500 yvar.long_name = 'latitude' 

1501 yvar.units = 'degrees_north' 

1502 

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

1504 

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

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

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

1508 

1509 nc.close() 

1510 

1511 

1512def to_array(var): 

1513 arr = var[:].copy() 

1514 if hasattr(var, 'scale_factor'): 

1515 arr *= var.scale_factor 

1516 

1517 if hasattr(var, 'add_offset'): 

1518 arr += var.add_offset 

1519 

1520 return arr 

1521 

1522 

1523def loadgrd(filename): 

1524 ''' 

1525 Read COARDS compliant netcdf (grd) file. 

1526 ''' 

1527 

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

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

1530 kx = 'x' 

1531 ky = 'y' 

1532 if 'lon' in vkeys: 

1533 kx = 'lon' 

1534 if 'lat' in vkeys: 

1535 ky = 'lat' 

1536 

1537 kz = 'z' 

1538 if 'altitude' in vkeys: 

1539 kz = 'altitude' 

1540 

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

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

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

1544 

1545 nc.close() 

1546 return x, y, z 

1547 

1548 

1549def centers_to_edges(asorted): 

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

1551 

1552 

1553def nvals(asorted): 

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

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

1556 

1557 

1558def guess_vals(asorted): 

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

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

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

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

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

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

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

1566 

1567 

1568def blockmean(asorted, b): 

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

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

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

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

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

1574 return ( 

1575 asorted[indis[:-1]], 

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

1577 

1578 

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

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

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

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

1583 

1584 zindi = yindi*nx+xindi 

1585 order = num.argsort(zindi) 

1586 z = z[order] 

1587 zindi = zindi[order] 

1588 

1589 zindi, z = blockmean(zindi, z) 

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

1591 znew[:] = num.nan 

1592 znew[zindi] = z 

1593 return znew.reshape(ny, nx) 

1594 

1595 

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

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

1598 xs = x_sorted 

1599 ys = y_sorted 

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

1601 if mode == 'nonrandom': 

1602 return nxs, nys, 0 

1603 elif xs.size == nxs*nys: 

1604 # exact match 

1605 return nxs, nys, 0 

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

1607 # possibly randomly sampled 

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

1609 nys = nxs 

1610 return nxs, nys, 2 

1611 else: 

1612 return nxs, nys, 1 

1613 

1614 

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

1616 ''' 

1617 Grid tabular XYZ data by binning. 

1618 

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

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

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

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

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

1624 

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

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

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

1628 ''' 

1629 

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

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

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

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

1634 if badness <= 1: 

1635 xf = guess_vals(xs) 

1636 yf = guess_vals(ys) 

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

1638 else: 

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

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

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

1642 

1643 return xf, yf, zf 

1644 

1645 

1646def tabledata(xf, yf, zf): 

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

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

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

1650 z = zf.flatten() 

1651 return x, y, z 

1652 

1653 

1654def double1d(a): 

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

1656 a2[::2] = a 

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

1658 return a2 

1659 

1660 

1661def double2d(f): 

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

1663 f2[:, :] = num.nan 

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

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

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

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

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

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

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

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

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

1673 return f2 

1674 

1675 

1676def doublegrid(x, y, z): 

1677 x2 = double1d(x) 

1678 y2 = double1d(y) 

1679 z2 = double2d(z) 

1680 return x2, y2, z2 

1681 

1682 

1683class Guru(object): 

1684 ''' 

1685 Abstract base class providing template interpolation, accessible as 

1686 attributes. 

1687 

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

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

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

1691 with the templates. 

1692 ''' 

1693 

1694 def __init__(self): 

1695 self.templates = {} 

1696 

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

1698 params = self.get_params(**kwargs) 

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

1700 return strings 

1701 

1702 # hand through templates dict 

1703 def __getitem__(self, template_name): 

1704 return self.templates[template_name] 

1705 

1706 def __setitem__(self, template_name, template): 

1707 self.templates[template_name] = template 

1708 

1709 def __contains__(self, template_name): 

1710 return template_name in self.templates 

1711 

1712 def __iter__(self): 

1713 return iter(self.templates) 

1714 

1715 def __len__(self): 

1716 return len(self.templates) 

1717 

1718 def __delitem__(self, template_name): 

1719 del(self.templates[template_name]) 

1720 

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

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

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

1724 

1725 def __getattr__(self, template_names): 

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

1727 raise AttributeError(template_names) 

1728 

1729 def f(**kwargs): 

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

1731 

1732 return f 

1733 

1734 

1735def nice_value(x): 

1736 ''' 

1737 Round ``x`` to nice value. 

1738 ''' 

1739 

1740 exp = 1.0 

1741 sign = 1 

1742 if x < 0.0: 

1743 x = -x 

1744 sign = -1 

1745 while x >= 1.0: 

1746 x /= 10.0 

1747 exp *= 10.0 

1748 while x < 0.1: 

1749 x *= 10.0 

1750 exp /= 10.0 

1751 

1752 if x >= 0.75: 

1753 return sign * 1.0 * exp 

1754 if x >= 0.375: 

1755 return sign * 0.5 * exp 

1756 if x >= 0.225: 

1757 return sign * 0.25 * exp 

1758 if x >= 0.15: 

1759 return sign * 0.2 * exp 

1760 

1761 return sign * 0.1 * exp 

1762 

1763 

1764class AutoScaler(object): 

1765 ''' 

1766 Tunable 1D autoscaling based on data range. 

1767 

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

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

1770 notation. 

1771 

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

1773 

1774 .. py:attribute:: approx_ticks 

1775 

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

1777 

1778 .. py:attribute:: mode 

1779 

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

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

1782 

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

1784 mode description 

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

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

1787 below. 

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

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

1790 max. 

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

1792 zero. 

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

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

1795 disabled, such that the output range always 

1796 exactly matches the data range. 

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

1798 

1799 .. py:attribute:: exp 

1800 

1801 If defined, override automatically determined exponent for notation 

1802 by the given value. 

1803 

1804 .. py:attribute:: snap 

1805 

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

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

1808 

1809 .. py:attribute:: inc 

1810 

1811 If defined, override automatically determined tick increment by the 

1812 given value. 

1813 

1814 .. py:attribute:: space 

1815 

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

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

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

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

1820 

1821 .. py:attribute:: exp_factor 

1822 

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

1824 

1825 .. py:attribute:: no_exp_interval: 

1826 

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

1828 

1829 ''' 

1830 

1831 def __init__( 

1832 self, 

1833 approx_ticks=7.0, 

1834 mode='auto', 

1835 exp=None, 

1836 snap=False, 

1837 inc=None, 

1838 space=0.0, 

1839 exp_factor=3, 

1840 no_exp_interval=(-3, 5)): 

1841 

1842 ''' 

1843 Create new AutoScaler instance. 

1844 

1845 The parameters are described in the AutoScaler documentation. 

1846 ''' 

1847 

1848 self.approx_ticks = approx_ticks 

1849 self.mode = mode 

1850 self.exp = exp 

1851 self.snap = snap 

1852 self.inc = inc 

1853 self.space = space 

1854 self.exp_factor = exp_factor 

1855 self.no_exp_interval = no_exp_interval 

1856 

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

1858 

1859 ''' 

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

1861 

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

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

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

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

1866 value. 

1867 ''' 

1868 

1869 data_min = min(data_range) 

1870 data_max = max(data_range) 

1871 

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

1873 

1874 a = self.mode 

1875 if self.mode == 'auto': 

1876 a = self.guess_autoscale_mode(data_min, data_max) 

1877 

1878 if override_mode is not None: 

1879 a = override_mode 

1880 

1881 mi, ma = 0, 0 

1882 if a == 'off': 

1883 mi, ma = data_min, data_max 

1884 elif a == '0-max': 

1885 mi = 0.0 

1886 if data_max > 0.0: 

1887 ma = data_max 

1888 else: 

1889 ma = 1.0 

1890 elif a == 'min-0': 

1891 ma = 0.0 

1892 if data_min < 0.0: 

1893 mi = data_min 

1894 else: 

1895 mi = -1.0 

1896 elif a == 'min-max': 

1897 mi, ma = data_min, data_max 

1898 elif a == 'symmetric': 

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

1900 mi = -m 

1901 ma = m 

1902 

1903 nmi = mi 

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

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

1906 

1907 nma = ma 

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

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

1910 

1911 mi, ma = nmi, nma 

1912 

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

1914 mi -= 1.0 

1915 ma += 1.0 

1916 

1917 # make nice tick increment 

1918 if self.inc is not None: 

1919 inc = self.inc 

1920 else: 

1921 if self.approx_ticks > 0.: 

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

1923 else: 

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

1925 

1926 if inc == 0.0: 

1927 inc = 1.0 

1928 

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

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

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

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

1933 

1934 if is_reverse: 

1935 return ma, mi, -inc 

1936 else: 

1937 return mi, ma, inc 

1938 

1939 def make_exp(self, x): 

1940 ''' 

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

1942 

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

1944 ''' 

1945 

1946 if self.exp is not None: 

1947 return self.exp 

1948 

1949 x = abs(x) 

1950 if x == 0.0: 

1951 return 0 

1952 

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

1954 return 0 

1955 

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

1957 

1958 def guess_autoscale_mode(self, data_min, data_max): 

1959 ''' 

1960 Guess mode of operation, based on data range. 

1961 

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

1963 or ``'symmetric'``. 

1964 ''' 

1965 

1966 a = 'min-max' 

1967 if data_min >= 0.0: 

1968 if data_min < data_max/2.: 

1969 a = '0-max' 

1970 else: 

1971 a = 'min-max' 

1972 if data_max <= 0.0: 

1973 if data_max > data_min/2.: 

1974 a = 'min-0' 

1975 else: 

1976 a = 'min-max' 

1977 if data_min < 0.0 and data_max > 0.0: 

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

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

1980 a = 'symmetric' 

1981 else: 

1982 a = 'min-max' 

1983 return a 

1984 

1985 

1986class Ax(AutoScaler): 

1987 ''' 

1988 Ax description with autoscaling capabilities. 

1989 

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

1991 the following additional attributes (with default values given in 

1992 paranthesis): 

1993 

1994 .. py:attribute:: label 

1995 

1996 Ax label (without unit). 

1997 

1998 .. py:attribute:: unit 

1999 

2000 Physical unit of the data attached to this ax. 

2001 

2002 .. py:attribute:: scaled_unit 

2003 

2004 (see below) 

2005 

2006 .. py:attribute:: scaled_unit_factor 

2007 

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

2009 

2010 unit = scaled_unit_factor x scaled_unit. 

2011 

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

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

2014 1e9.) 

2015 

2016 .. py:attribute:: limits 

2017 

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

2019 

2020 .. py:attribute:: masking 

2021 

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

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

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

2025 

2026 ''' 

2027 

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

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

2030 

2031 AutoScaler.__init__(self, **kwargs) 

2032 self.label = label 

2033 self.unit = unit 

2034 self.scaled_unit_factor = scaled_unit_factor 

2035 self.scaled_unit = scaled_unit 

2036 self.limits = limits 

2037 self.masking = masking 

2038 

2039 def label_str(self, exp, unit): 

2040 ''' 

2041 Get label string including the unit and multiplier. 

2042 ''' 

2043 

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

2045 if self.label: 

2046 slabel = self.label 

2047 

2048 if unit or exp != 0: 

2049 if exp != 0: 

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

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

2052 else: 

2053 sunit = '[ %s ]' % unit 

2054 

2055 p = [] 

2056 if slabel: 

2057 p.append(slabel) 

2058 

2059 if sunit: 

2060 p.append(sunit) 

2061 

2062 return ' '.join(p) 

2063 

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

2065 override_scaled_unit_factor=None): 

2066 

2067 ''' 

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

2069 

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

2071 multiplier for given data range. 

2072 

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

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

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

2076 scaling applied. 

2077 ''' 

2078 

2079 sf = self.scaled_unit_factor 

2080 

2081 if override_scaled_unit_factor is not None: 

2082 sf = override_scaled_unit_factor 

2083 

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

2085 

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

2087 if self.inc is not None: 

2088 inc = self.inc*sf 

2089 

2090 if ax_projection: 

2091 exp = self.make_exp(inc) 

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

2093 unit = self.unit 

2094 else: 

2095 unit = self.scaled_unit 

2096 label = self.label_str(exp, unit) 

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

2098 else: 

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

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

2101 

2102 

2103class ScaleGuru(Guru): 

2104 

2105 ''' 

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

2107 

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

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

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

2111 arguments, which are required for most GMT commands. 

2112 

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

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

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

2116 

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

2118 

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

2120 limits imposed on other axes. 

2121 

2122 ''' 

2123 

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

2125 percent_interval=None, copy_from=None): 

2126 

2127 Guru.__init__(self) 

2128 

2129 if copy_from: 

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

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

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

2133 self.aspect = copy_from.aspect 

2134 

2135 if percent_interval is not None: 

2136 from scipy.stats import scoreatpercentile as scap 

2137 

2138 self.templates = dict( 

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

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

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

2142 

2143 maxdim = 2 

2144 if data_tuples: 

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

2146 else: 

2147 if axes: 

2148 maxdim = len(axes) 

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

2150 if axes is not None: 

2151 self.axes = axes 

2152 else: 

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

2154 

2155 # sophisticated data-range calculation 

2156 data_ranges = [None] * maxdim 

2157 for dt_ in data_tuples: 

2158 dt = num.asarray(dt_) 

2159 in_range = True 

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

2161 if ax.limits and ax.masking: 

2162 ax_limits = list(ax.limits) 

2163 if ax_limits[0] is None: 

2164 ax_limits[0] = -num.inf 

2165 if ax_limits[1] is None: 

2166 ax_limits[1] = num.inf 

2167 in_range = num.logical_and( 

2168 in_range, 

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

2170 

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

2172 

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

2174 if len(x) >= 1: 

2175 if in_range is not True: 

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

2177 if percent_interval is None: 

2178 range_this = ( 

2179 num.nanmin(xmasked), 

2180 num.nanmax(xmasked)) 

2181 else: 

2182 xmasked_finite = num.compress( 

2183 num.isfinite(xmasked), xmasked) 

2184 range_this = ( 

2185 scap(xmasked_finite, 

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

2187 scap(xmasked_finite, 

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

2189 else: 

2190 if percent_interval is None: 

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

2192 else: 

2193 xmasked_finite = num.compress( 

2194 num.isfinite(xmasked), xmasked) 

2195 range_this = ( 

2196 scap(xmasked_finite, 

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

2198 scap(xmasked_finite, 

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

2200 else: 

2201 range_this = (0., 1.) 

2202 

2203 if ax.limits: 

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

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

2206 range_this[1]) 

2207 

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

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

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

2211 

2212 else: 

2213 range_this = ax.limits 

2214 

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

2216 data_ranges[i] = range_this 

2217 else: 

2218 mi, ma = range_this 

2219 if data_ranges[i] is not None: 

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

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

2222 

2223 data_ranges[i] = (mi, ma) 

2224 

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

2226 if data_ranges[i] is None or not ( 

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

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

2229 

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

2231 

2232 self.data_ranges = data_ranges 

2233 self.aspect = aspect 

2234 

2235 def copy(self): 

2236 return ScaleGuru(copy_from=self) 

2237 

2238 def get_params(self, ax_projection=False): 

2239 

2240 ''' 

2241 Get dict with output parameters. 

2242 

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

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

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

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

2247 

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

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

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

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

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

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

2254 label string. 

2255 ''' 

2256 

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

2258 self.data_ranges[0], ax_projection) 

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

2260 self.data_ranges[1], ax_projection) 

2261 if len(self.axes) > 2: 

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

2263 self.data_ranges[2], ax_projection) 

2264 

2265 # enforce certain aspect, if needed 

2266 if self.aspect is not None: 

2267 xwid = xma-xmi 

2268 ywid = yma-ymi 

2269 if ywid < xwid*self.aspect: 

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

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

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

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

2274 override_scaled_unit_factor=1.) 

2275 

2276 elif xwid < ywid/self.aspect: 

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

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

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

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

2281 override_scaled_unit_factor=1.) 

2282 

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

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

2285 if len(self.axes) > 2: 

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

2287 

2288 return params 

2289 

2290 

2291class GumSpring(object): 

2292 

2293 ''' 

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

2295 ''' 

2296 

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

2298 self.minimal = minimal 

2299 if grow is None: 

2300 if minimal is None: 

2301 self.grow = 1.0 

2302 else: 

2303 self.grow = 0.0 

2304 else: 

2305 self.grow = grow 

2306 self.value = 1.0 

2307 

2308 def get_minimal(self): 

2309 if self.minimal is not None: 

2310 return self.minimal 

2311 else: 

2312 return 0.0 

2313 

2314 def get_grow(self): 

2315 return self.grow 

2316 

2317 def set_value(self, value): 

2318 self.value = value 

2319 

2320 def get_value(self): 

2321 return self.value 

2322 

2323 

2324def distribute(sizes, grows, space): 

2325 sizes = list(sizes) 

2326 gsum = sum(grows) 

2327 if gsum > 0.0: 

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

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

2330 return sizes 

2331 

2332 

2333class Widget(Guru): 

2334 

2335 ''' 

2336 Base class of the gmtpy layout system. 

2337 

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

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

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

2341 

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

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

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

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

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

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

2348 

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

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

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

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

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

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

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

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

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

2358 

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

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

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

2362 ''' 

2363 

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

2365 

2366 ''' 

2367 Create new widget. 

2368 ''' 

2369 

2370 Guru.__init__(self) 

2371 

2372 self.templates = dict( 

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

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

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

2376 

2377 if horizontal is None: 

2378 self.horizontal = GumSpring() 

2379 else: 

2380 self.horizontal = horizontal 

2381 

2382 if vertical is None: 

2383 self.vertical = GumSpring() 

2384 else: 

2385 self.vertical = vertical 

2386 

2387 self.aspect = None 

2388 self.parent = parent 

2389 self.dirty = True 

2390 

2391 def set_parent(self, parent): 

2392 

2393 ''' 

2394 Set the parent widget. 

2395 

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

2397 methods are responsible for calling this. 

2398 ''' 

2399 

2400 self.parent = parent 

2401 self.dirtyfy() 

2402 

2403 def get_parent(self): 

2404 

2405 ''' 

2406 Get the widgets parent widget. 

2407 ''' 

2408 

2409 return self.parent 

2410 

2411 def get_root(self): 

2412 

2413 ''' 

2414 Get the root widget in the layout hierarchy. 

2415 ''' 

2416 

2417 if self.parent is not None: 

2418 return self.get_parent() 

2419 else: 

2420 return self 

2421 

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

2423 

2424 ''' 

2425 Set the horizontal sizing policy of the Widget. 

2426 

2427 

2428 :param minimal: new minimal width of the widget 

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

2430 ''' 

2431 

2432 self.horizontal = GumSpring(minimal, grow) 

2433 self.dirtyfy() 

2434 

2435 def get_horizontal(self): 

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

2437 

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

2439 

2440 ''' 

2441 Set the horizontal sizing policy of the Widget. 

2442 

2443 :param minimal: new minimal height of the widget 

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

2445 ''' 

2446 

2447 self.vertical = GumSpring(minimal, grow) 

2448 self.dirtyfy() 

2449 

2450 def get_vertical(self): 

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

2452 

2453 def set_aspect(self, aspect=None): 

2454 

2455 ''' 

2456 Set aspect constraint on the widget. 

2457 

2458 The aspect is given as height divided by width. 

2459 ''' 

2460 

2461 self.aspect = aspect 

2462 self.dirtyfy() 

2463 

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

2465 

2466 ''' 

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

2468 call. 

2469 ''' 

2470 

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

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

2473 self.set_aspect(aspect) 

2474 

2475 def get_policy(self): 

2476 mh, gh = self.get_horizontal() 

2477 mv, gv = self.get_vertical() 

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

2479 

2480 def legalize(self, size, offset): 

2481 

2482 ''' 

2483 Get legal size for widget. 

2484 

2485 Returns: (new_size, new_offset) 

2486 

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

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

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

2490 ''' 

2491 

2492 sh, sv = size 

2493 oh, ov = offset 

2494 shs, svs = Widget.get_min_size(self) 

2495 ghs, gvs = Widget.get_grow(self) 

2496 

2497 if ghs == 0.0: 

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

2499 sh = shs 

2500 

2501 if gvs == 0.0: 

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

2503 sv = svs 

2504 

2505 if self.aspect is not None: 

2506 if sh > sv/self.aspect: 

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

2508 sh = sv/self.aspect 

2509 if sv > sh*self.aspect: 

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

2511 sv = sh*self.aspect 

2512 

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

2514 

2515 def get_min_size(self): 

2516 

2517 ''' 

2518 Get minimum size of widget. 

2519 

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

2521 ''' 

2522 

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

2524 if self.aspect is not None: 

2525 if mv == 0.0: 

2526 return mh, mh*self.aspect 

2527 elif mh == 0.0: 

2528 return mv/self.aspect, mv 

2529 return mh, mv 

2530 

2531 def get_grow(self): 

2532 

2533 ''' 

2534 Get widget's desire to grow. 

2535 

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

2537 ''' 

2538 

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

2540 

2541 def set_size(self, size, offset): 

2542 

2543 ''' 

2544 Set the widget's current size. 

2545 

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

2547 responsibility to call this. 

2548 ''' 

2549 

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

2551 self.offset = inner_offset 

2552 self.horizontal.set_value(sh) 

2553 self.vertical.set_value(sv) 

2554 self.dirty = False 

2555 

2556 def __str__(self): 

2557 

2558 def indent(ind, str): 

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

2560 size, offset = self.get_size() 

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

2562 children = self.get_children() 

2563 if children: 

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

2565 return s 

2566 

2567 def policies_debug_str(self): 

2568 

2569 def indent(ind, str): 

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

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

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

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

2574 

2575 children = self.get_children() 

2576 if children: 

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

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

2579 return s 

2580 

2581 def get_corners(self, descend=False): 

2582 

2583 ''' 

2584 Get coordinates of the corners of the widget. 

2585 

2586 Returns list with coordinate tuples. 

2587 

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

2589 coordinates of all sub-widgets. 

2590 ''' 

2591 

2592 self.do_layout() 

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

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

2595 if descend: 

2596 for child in self.get_children(): 

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

2598 return corners 

2599 

2600 def get_sizes(self): 

2601 

2602 ''' 

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

2604 

2605 Returns a list with size tuples. 

2606 ''' 

2607 self.do_layout() 

2608 sizes = [self.get_size()] 

2609 for child in self.get_children(): 

2610 sizes.extend(child.get_sizes()) 

2611 return sizes 

2612 

2613 def do_layout(self): 

2614 

2615 ''' 

2616 Triggers layouting of the widget hierarchy, if needed. 

2617 ''' 

2618 

2619 if self.parent is not None: 

2620 return self.parent.do_layout() 

2621 

2622 if not self.dirty: 

2623 return 

2624 

2625 sh, sv = self.get_min_size() 

2626 gh, gv = self.get_grow() 

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

2628 sh = 15.*cm 

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

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

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

2632 

2633 def get_children(self): 

2634 

2635 ''' 

2636 Get sub-widgets contained in this widget. 

2637 

2638 Returns a list of widgets. 

2639 ''' 

2640 

2641 return [] 

2642 

2643 def get_size(self): 

2644 

2645 ''' 

2646 Get current size and position of the widget. 

2647 

2648 Triggers layouting and returns 

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

2650 ''' 

2651 

2652 self.do_layout() 

2653 return (self.horizontal.get_value(), 

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

2655 

2656 def get_params(self): 

2657 

2658 ''' 

2659 Get current size and position of the widget. 

2660 

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

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

2663 ''' 

2664 

2665 self.do_layout() 

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

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

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

2669 

2670 def width(self): 

2671 

2672 ''' 

2673 Get current width of the widget. 

2674 

2675 Triggers layouting and returns width. 

2676 ''' 

2677 

2678 self.do_layout() 

2679 return self.horizontal.get_value() 

2680 

2681 def height(self): 

2682 

2683 ''' 

2684 Get current height of the widget. 

2685 

2686 Triggers layouting and return height. 

2687 ''' 

2688 

2689 self.do_layout() 

2690 return self.vertical.get_value() 

2691 

2692 def bbox(self): 

2693 

2694 ''' 

2695 Get PostScript bounding box for this widget. 

2696 

2697 Triggers layouting and returns values suitable to create PS bounding 

2698 box, representing the widgets current size and position. 

2699 ''' 

2700 

2701 self.do_layout() 

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

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

2704 

2705 def dirtyfy(self): 

2706 

2707 ''' 

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

2709 

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

2711 new layouting. 

2712 ''' 

2713 

2714 if self.parent is not None: 

2715 self.parent.dirtyfy() 

2716 

2717 self.dirty = True 

2718 

2719 

2720class CenterLayout(Widget): 

2721 

2722 ''' 

2723 A layout manager which centers its single child widget. 

2724 

2725 The child widget may be oversized. 

2726 ''' 

2727 

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

2729 Widget.__init__(self, horizontal, vertical) 

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

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

2732 

2733 def get_min_size(self): 

2734 shs, svs = Widget.get_min_size(self) 

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

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

2737 

2738 def get_grow(self): 

2739 ghs, gvs = Widget.get_grow(self) 

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

2741 return gh*ghs, gv*gvs 

2742 

2743 def set_size(self, size, offset): 

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

2745 

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

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

2748 if ghc != 0.: 

2749 shc = sh 

2750 if gvc != 0.: 

2751 svc = sv 

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

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

2754 

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

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

2757 

2758 def set_widget(self, widget=None): 

2759 

2760 ''' 

2761 Set the child widget, which shall be centered. 

2762 ''' 

2763 

2764 if widget is None: 

2765 widget = Widget() 

2766 

2767 self.content = widget 

2768 

2769 widget.set_parent(self) 

2770 

2771 def get_widget(self): 

2772 return self.content 

2773 

2774 def get_children(self): 

2775 return [self.content] 

2776 

2777 

2778class FrameLayout(Widget): 

2779 

2780 ''' 

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

2782 widgets. 

2783 

2784 :: 

2785 

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

2787 | top | 

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

2789 | | | | 

2790 | left | center | right | 

2791 | | | | 

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

2793 | bottom | 

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

2795 

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

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

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

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

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

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

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

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

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

2805 spaces between the widgets. 

2806 ''' 

2807 

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

2809 Widget.__init__(self, horizontal, vertical) 

2810 mw = 3.*cm 

2811 self.left = Widget( 

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

2813 self.right = Widget( 

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

2815 self.top = Widget( 

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

2817 parent=self) 

2818 self.bottom = Widget( 

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

2820 parent=self) 

2821 self.center = Widget( 

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

2823 parent=self) 

2824 

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

2826 ''' 

2827 Give margins fixed size constraints. 

2828 ''' 

2829 

2830 self.left.set_horizontal(left, 0) 

2831 self.right.set_horizontal(right, 0) 

2832 self.top.set_vertical(top, 0) 

2833 self.bottom.set_vertical(bottom, 0) 

2834 

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

2836 ''' 

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

2838 

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

2840 ''' 

2841 self.left.set_horizontal(left, grow) 

2842 self.right.set_horizontal(right, grow) 

2843 self.top.set_vertical(top, grow) 

2844 self.bottom.set_vertical(bottom, grow) 

2845 

2846 def get_min_size(self): 

2847 shs, svs = Widget.get_min_size(self) 

2848 

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

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

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

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

2853 

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

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

2856 

2857 # prevent widgets from collapsing 

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

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

2860 shsum += 0.1*cm 

2861 

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

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

2864 svsum += 0.1*cm 

2865 

2866 sh = max(shs, shsum) 

2867 sv = max(svs, svsum) 

2868 

2869 return sh, sv 

2870 

2871 def get_grow(self): 

2872 ghs, gvs = Widget.get_grow(self) 

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

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

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

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

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

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

2879 return gh, gv 

2880 

2881 def set_size(self, size, offset): 

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

2883 

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

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

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

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

2888 

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

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

2891 

2892 if ah < 0.0: 

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

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

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

2896 if av < 0.0: 

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

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

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

2900 

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

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

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

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

2905 

2906 if self.center.aspect is not None: 

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

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

2909 if 0.0 < ahm < ah: 

2910 slh, srh, sch = distribute( 

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

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

2913 

2914 elif 0.0 < avm < av: 

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

2916 sch*self.center.aspect), 

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

2918 

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

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

2921 

2922 oh += ah/2. 

2923 ov += av/2. 

2924 sh -= ah 

2925 sv -= av 

2926 

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

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

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

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

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

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

2933 

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

2935 

2936 ''' 

2937 Set one of the sub-widgets. 

2938 

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

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

2941 ''' 

2942 

2943 if widget is None: 

2944 widget = Widget() 

2945 

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

2947 self.__dict__[which] = widget 

2948 else: 

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

2950 

2951 widget.set_parent(self) 

2952 

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

2954 

2955 ''' 

2956 Get one of the sub-widgets. 

2957 

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

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

2960 ''' 

2961 

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

2963 return self.__dict__[which] 

2964 else: 

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

2966 

2967 def get_children(self): 

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

2969 

2970 

2971class GridLayout(Widget): 

2972 

2973 ''' 

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

2975 

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

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

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

2979 

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

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

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

2983 might not be resolved optimally. 

2984 ''' 

2985 

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

2987 

2988 ''' 

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

2990 ''' 

2991 

2992 Widget.__init__(self, horizontal, vertical) 

2993 self.grid = [] 

2994 for iy in range(ny): 

2995 row = [] 

2996 for ix in range(nx): 

2997 w = Widget(parent=self) 

2998 row.append(w) 

2999 

3000 self.grid.append(row) 

3001 

3002 def sub_min_sizes_as_array(self): 

3003 esh = num.array( 

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

3005 dtype=float) 

3006 esv = num.array( 

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

3008 dtype=float) 

3009 return esh, esv 

3010 

3011 def sub_grows_as_array(self): 

3012 egh = num.array( 

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

3014 dtype=float) 

3015 egv = num.array( 

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

3017 dtype=float) 

3018 return egh, egv 

3019 

3020 def get_min_size(self): 

3021 sh, sv = Widget.get_min_size(self) 

3022 esh, esv = self.sub_min_sizes_as_array() 

3023 if esh.size != 0: 

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

3025 if esv.size != 0: 

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

3027 return sh, sv 

3028 

3029 def get_grow(self): 

3030 ghs, gvs = Widget.get_grow(self) 

3031 egh, egv = self.sub_grows_as_array() 

3032 if egh.size != 0: 

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

3034 else: 

3035 gh = 1.0 

3036 if egv.size != 0: 

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

3038 else: 

3039 gv = 1.0 

3040 return gh, gv 

3041 

3042 def set_size(self, size, offset): 

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

3044 esh, esv = self.sub_min_sizes_as_array() 

3045 egh, egv = self.sub_grows_as_array() 

3046 

3047 # available additional space 

3048 empty = esh.size == 0 

3049 

3050 if not empty: 

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

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

3053 else: 

3054 av = sv 

3055 ah = sh 

3056 

3057 if ah < 0.0: 

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

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

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

3061 if av < 0.0: 

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

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

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

3065 

3066 nx, ny = esh.shape 

3067 

3068 if not empty: 

3069 # distribute additional space on rows and columns 

3070 # according to grow weights and minimal sizes 

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

3072 nesh = esh.copy() 

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

3074 

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

3076 

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

3078 nesv = esv.copy() 

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

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

3081 

3082 ah = sh - sum(nsh) 

3083 av = sv - sum(nsv) 

3084 

3085 oh += ah/2. 

3086 ov += av/2. 

3087 sh -= ah 

3088 sv -= av 

3089 

3090 # resize child widgets 

3091 neov = ov + sum(nsv) 

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

3093 neov -= nesv 

3094 neoh = oh 

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

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

3097 neoh += nesh 

3098 

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

3100 

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

3102 

3103 ''' 

3104 Set one of the sub-widgets. 

3105 

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

3107 counted from zero. 

3108 ''' 

3109 

3110 if widget is None: 

3111 widget = Widget() 

3112 

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

3114 widget.set_parent(self) 

3115 

3116 def get_widget(self, ix, iy): 

3117 

3118 ''' 

3119 Get one of the sub-widgets. 

3120 

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

3122 counted from zero. 

3123 ''' 

3124 

3125 return self.grid[iy][ix] 

3126 

3127 def get_children(self): 

3128 children = [] 

3129 for row in self.grid: 

3130 children.extend(row) 

3131 

3132 return children 

3133 

3134 

3135def is_gmt5(version='newest'): 

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

3137 

3138 

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

3140 

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

3142 

3143 if gmt.is_gmt5(): 

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

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

3146 gmt.save(fn, crop_eps_mode=True) 

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

3148 s = f.read() 

3149 

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

3151 else: 

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

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

3154 

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

3156 

3157 

3158def text_box( 

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

3160 

3161 gmt = GMT(version=gmtversion) 

3162 if gmt.is_gmt5(): 

3163 row = [0, 0, text] 

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

3165 else: 

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

3167 farg = [] 

3168 

3169 gmt.pstext( 

3170 in_rows=[row], 

3171 finish=True, 

3172 R=(0, 1, 0, 1), 

3173 J='x10p', 

3174 N=True, 

3175 *farg, 

3176 **kwargs) 

3177 

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

3179 gmt.save(fn) 

3180 

3181 (_, stderr) = subprocess.Popen( 

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

3183 stderr=subprocess.PIPE).communicate() 

3184 

3185 dx, dy = None, None 

3186 for line in stderr.splitlines(): 

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

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

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

3190 break 

3191 

3192 return dx, dy 

3193 

3194 

3195class TableLiner(object): 

3196 ''' 

3197 Utility class to turn tables into lines. 

3198 ''' 

3199 

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

3201 self.in_columns = in_columns 

3202 self.in_rows = in_rows 

3203 self.encoding = encoding 

3204 

3205 def __iter__(self): 

3206 if self.in_columns is not None: 

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

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

3209 self.encoding) 

3210 

3211 if self.in_rows is not None: 

3212 for row in self.in_rows: 

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

3214 self.encoding) 

3215 

3216 

3217class LineStreamChopper(object): 

3218 ''' 

3219 File-like object to buffer data. 

3220 ''' 

3221 

3222 def __init__(self, liner): 

3223 self.chopsize = None 

3224 self.liner = liner 

3225 self.chop_iterator = None 

3226 self.closed = False 

3227 

3228 def _chopiter(self): 

3229 buf = BytesIO() 

3230 for line in self.liner: 

3231 buf.write(line) 

3232 buflen = buf.tell() 

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

3234 buf.seek(0) 

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

3236 yield buf.read(self.chopsize) 

3237 

3238 newbuf = BytesIO() 

3239 newbuf.write(buf.read()) 

3240 buf.close() 

3241 buf = newbuf 

3242 

3243 yield(buf.getvalue()) 

3244 buf.close() 

3245 

3246 def read(self, size=None): 

3247 if self.closed: 

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

3249 if self.chop_iterator is None: 

3250 self.chopsize = size 

3251 self.chop_iterator = self._chopiter() 

3252 

3253 self.chopsize = size 

3254 try: 

3255 return next(self.chop_iterator) 

3256 except StopIteration: 

3257 return '' 

3258 

3259 def close(self): 

3260 self.chopsize = None 

3261 self.chop_iterator = None 

3262 self.closed = True 

3263 

3264 def flush(self): 

3265 pass 

3266 

3267 

3268font_tab = { 

3269 0: 'Helvetica', 

3270 1: 'Helvetica-Bold', 

3271} 

3272 

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

3274 

3275 

3276class GMT(object): 

3277 ''' 

3278 A thin wrapper to GMT command execution. 

3279 

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

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

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

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

3284 gmtpy and gmtpy must know where to find it. 

3285 

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

3287 output file. 

3288 

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

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

3291 

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

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

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

3295 

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

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

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

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

3300 

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

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

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

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

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

3306 execution of more than one GMT instance. 

3307 

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

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

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

3311 backward compatibility of the scripts can be maintained. 

3312 

3313 ''' 

3314 

3315 def __init__( 

3316 self, 

3317 config=None, 

3318 kontinue=None, 

3319 version='newest', 

3320 config_papersize=None, 

3321 eps_mode=False): 

3322 

3323 self.installation = get_gmt_installation(version) 

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

3325 self.eps_mode = eps_mode 

3326 self._shutil = shutil 

3327 

3328 if config: 

3329 self.gmt_config.update(config) 

3330 

3331 if config_papersize: 

3332 if not isinstance(config_papersize, str): 

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

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

3335 

3336 if self.is_gmt5(): 

3337 self.gmt_config['PS_MEDIA'] = config_papersize 

3338 else: 

3339 self.gmt_config['PAPER_MEDIA'] = config_papersize 

3340 

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

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

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

3344 

3345 if kontinue is not None: 

3346 self.load_unfinished(kontinue) 

3347 self.needstart = False 

3348 else: 

3349 self.output = BytesIO() 

3350 self.needstart = True 

3351 

3352 self.finished = False 

3353 

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

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

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

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

3358 

3359 self.layout = None 

3360 self.command_log = [] 

3361 self.keep_temp_dir = False 

3362 

3363 def is_gmt5(self): 

3364 return self.installation['version'][0] in ['5', '6'] 

3365 

3366 def get_version(self): 

3367 return self.installation['version'] 

3368 

3369 def get_config(self, key): 

3370 return self.gmt_config[key] 

3371 

3372 def to_points(self, string): 

3373 if not string: 

3374 return 0 

3375 

3376 unit = string[-1] 

3377 if unit in _units: 

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

3379 else: 

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

3381 return float(string)/_units[default_unit] 

3382 

3383 def label_font_size(self): 

3384 if self.is_gmt5(): 

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

3386 else: 

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

3388 

3389 def label_font(self): 

3390 if self.is_gmt5(): 

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

3392 else: 

3393 return self.gmt_config['LABEL_FONT'] 

3394 

3395 def gen_gmt_config_file(self, config_filename, config): 

3396 f = open(config_filename, 'wb') 

3397 f.write( 

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

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

3400 

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

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

3403 f.close() 

3404 

3405 def __del__(self): 

3406 if not self.keep_temp_dir: 

3407 self._shutil.rmtree(self.tempdir) 

3408 

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

3410 

3411 ''' 

3412 Execute arbitrary GMT command. 

3413 

3414 See docstring in __getattr__ for details. 

3415 ''' 

3416 

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

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

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

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

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

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

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

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

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

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

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

3428 

3429 assert(not self.finished) 

3430 

3431 # check for mutual exclusiveness on input and output possibilities 

3432 assert(1 >= len( 

3433 [x for x in [ 

3434 in_stream, in_filename, in_string, in_columns, in_rows] 

3435 if x is not None])) 

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

3437 if x is not None])) 

3438 

3439 options = [] 

3440 

3441 gmt_config = self.gmt_config 

3442 if not self.is_gmt5(): 

3443 gmt_config_filename = self.gmt_config_filename 

3444 if config_override: 

3445 gmt_config = self.gmt_config.copy() 

3446 gmt_config.update(config_override) 

3447 gmt_config_override_filename = pjoin( 

3448 self.tempdir, 'gmtdefaults_override') 

3449 self.gen_gmt_config_file( 

3450 gmt_config_override_filename, gmt_config) 

3451 gmt_config_filename = gmt_config_override_filename 

3452 

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

3454 if config_override: 

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

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

3457 

3458 if out_discard: 

3459 out_filename = '/dev/null' 

3460 

3461 out_mustclose = False 

3462 if out_filename is not None: 

3463 out_mustclose = True 

3464 out_stream = open(out_filename, 'wb') 

3465 

3466 if in_filename is not None: 

3467 in_stream = open(in_filename, 'rb') 

3468 

3469 if in_string is not None: 

3470 in_stream = BytesIO(in_string) 

3471 

3472 encoding_gmt = gmt_config.get( 

3473 'PS_CHAR_ENCODING', 

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

3475 

3476 encoding = encoding_gmt_to_python[encoding_gmt.lower()] 

3477 

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

3479 in_stream = LineStreamChopper(TableLiner(in_columns=in_columns, 

3480 in_rows=in_rows, 

3481 encoding=encoding)) 

3482 

3483 # convert option arguments to strings 

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

3485 if len(k) > 1: 

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

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

3488 % (k, command)) 

3489 

3490 if type(v) is bool: 

3491 if v: 

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

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

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

3495 else: 

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

3497 

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

3499 if out_stream is None: 

3500 if not finish: 

3501 options.append('-K') 

3502 else: 

3503 self.finished = True 

3504 

3505 if not self.needstart: 

3506 options.append('-O') 

3507 else: 

3508 self.needstart = False 

3509 

3510 out_stream = self.output 

3511 

3512 # run the command 

3513 if self.is_gmt5(): 

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

3515 else: 

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

3517 

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

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

3520 args.extend(options) 

3521 args.extend(addargs) 

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

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

3524 args.append('+'+gmt_config_filename) 

3525 

3526 bs = 2048 

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

3528 stdout=subprocess.PIPE, bufsize=bs, 

3529 env=self.environ) 

3530 while True: 

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

3532 if cr: 

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

3534 if cw: 

3535 if in_stream is not None: 

3536 data = in_stream.read(bs) 

3537 if len(data) == 0: 

3538 break 

3539 p.stdin.write(data) 

3540 else: 

3541 break 

3542 if not cr and not cw: 

3543 break 

3544 

3545 p.stdin.close() 

3546 

3547 while True: 

3548 data = p.stdout.read(bs) 

3549 if len(data) == 0: 

3550 break 

3551 out_stream.write(data) 

3552 

3553 p.stdout.close() 

3554 

3555 retcode = p.wait() 

3556 

3557 if in_stream is not None: 

3558 in_stream.close() 

3559 

3560 if out_mustclose: 

3561 out_stream.close() 

3562 

3563 if retcode != 0: 

3564 self.keep_temp_dir = True 

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

3566 'While executing command:\n%s' 

3567 % (command, escape_shell_args(args))) 

3568 

3569 self.command_log.append(args) 

3570 

3571 def __getattr__(self, command): 

3572 

3573 ''' 

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

3575 

3576 Execute arbitrary GMT command. 

3577 

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

3579 output file maintained by the GMT instance on which this method is 

3580 called. 

3581 

3582 Except for a few keyword arguments listed below, any ``kwargs`` and 

3583 ``addargs`` are converted into command line options and arguments and 

3584 passed to the GMT command. Numbers in keyword arguments are converted 

3585 into strings. E.g. ``S=10`` is translated into ``'-S10'``. Tuples of 

3586 numbers or strings are converted into strings where the elements of the 

3587 tuples are separated by slashes '/'. E.g. ``R=(10, 10, 20, 20)`` is 

3588 translated into ``'-R10/10/20/20'``. Options with a boolean argument 

3589 are only appended to the GMT command, if their values are True. 

3590 

3591 If no output redirection is in effect, the -K and -O options are 

3592 handled by gmtpy and thus should not be specified. Use 

3593 ``out_discard=True`` if you don't want -K or -O beeing added, but are 

3594 not interested in the output. 

3595 

3596 The standard input of the GMT process is fed by data selected with one 

3597 of the following ``in_*`` keyword arguments: 

3598 

3599 =============== ======================================================= 

3600 ``in_stream`` Data is read from an open file like object. 

3601 ``in_filename`` Data is read from the given file. 

3602 ``in_string`` String content is dumped to the process. 

3603 ``in_columns`` A 2D nested iterable whose elements can be accessed as 

3604 ``in_columns[icolumn][irow]`` is converted into an 

3605 ascii 

3606 table, which is fed to the process. 

3607 ``in_rows`` A 2D nested iterable whos elements can be accessed as 

3608 ``in_rows[irow][icolumn]`` is converted into an ascii 

3609 table, which is fed to the process. 

3610 =============== ======================================================= 

3611 

3612 The standard output of the GMT process may be redirected by one of the 

3613 following options: 

3614 

3615 ================= ===================================================== 

3616 ``out_stream`` Output is fed to an open file like object. 

3617 ``out_filename`` Output is dumped to the given file. 

3618 ``out_discard`` If True, output is dumped to :file:`/dev/null`. 

3619 ================= ===================================================== 

3620 

3621 Additional keyword arguments: 

3622 

3623 ===================== ================================================= 

3624 ``config`` Dict with GMT defaults which override the 

3625 currently active set of defaults exclusively 

3626 during this call. 

3627 ``finish`` If True, the postscript file, which is maintained 

3628 by the GMT instance is finished, and no further 

3629 plotting is allowed. 

3630 ``suppress_defaults`` Suppress appending of the ``'+gmtdefaults'`` 

3631 option to the command. 

3632 ===================== ================================================= 

3633 

3634 ''' 

3635 

3636 def f(*args, **kwargs): 

3637 return self._gmtcommand(command, *args, **kwargs) 

3638 return f 

3639 

3640 def tempfilename(self, name=None): 

3641 ''' 

3642 Get filename for temporary file in the private temp directory. 

3643 

3644 If no ``name`` argument is given, a random name is picked. If 

3645 ``name`` is given, returns a path ending in that ``name``. 

3646 ''' 

3647 

3648 if not name: 

3649 name = ''.join( 

3650 [random.choice('abcdefghijklmnopqrstuvwxyz') 

3651 for i in range(10)]) 

3652 

3653 fn = pjoin(self.tempdir, name) 

3654 return fn 

3655 

3656 def tempfile(self, name=None): 

3657 ''' 

3658 Create and open a file in the private temp directory. 

3659 ''' 

3660 

3661 fn = self.tempfilename(name) 

3662 f = open(fn, 'wb') 

3663 return f, fn 

3664 

3665 def save_unfinished(self, filename): 

3666 out = open(filename, 'wb') 

3667 out.write(self.output.getvalue()) 

3668 out.close() 

3669 

3670 def load_unfinished(self, filename): 

3671 self.output = BytesIO() 

3672 self.finished = False 

3673 inp = open(filename, 'rb') 

3674 self.output.write(inp.read()) 

3675 inp.close() 

3676 

3677 def dump(self, ident): 

3678 filename = self.tempfilename('breakpoint-%s' % ident) 

3679 self.save_unfinished(filename) 

3680 

3681 def load(self, ident): 

3682 filename = self.tempfilename('breakpoint-%s' % ident) 

3683 self.load_unfinished(filename) 

3684 

3685 def save(self, filename=None, bbox=None, resolution=150, oversample=2., 

3686 width=None, height=None, size=None, crop_eps_mode=False, 

3687 psconvert=False): 

3688 

3689 ''' 

3690 Finish and save figure as PDF, PS or PPM file. 

3691 

3692 If filename ends with ``'.pdf'`` a PDF file is created by piping the 

3693 GMT output through :program:`gmtpy-epstopdf`. 

3694 

3695 If filename ends with ``'.png'`` a PNG file is created by running 

3696 :program:`gmtpy-epstopdf`, :program:`pdftocairo` and 

3697 :program:`convert`. ``resolution`` specifies the resolution in DPI for 

3698 raster file formats. Rasterization is done at a higher resolution if 

3699 ``oversample`` is set to a value higher than one. The output image size 

3700 can also be controlled by setting ``width``, ``height`` or ``size`` 

3701 instead of ``resolution``. When ``size`` is given, the image is scaled 

3702 so that ``max(width, height) == size``. 

3703 

3704 The bounding box is set according to the values given in ``bbox``. 

3705 ''' 

3706 

3707 if not self.finished: 

3708 self.psxy(R=True, J=True, finish=True) 

3709 

3710 if filename: 

3711 tempfn = pjoin(self.tempdir, 'incomplete') 

3712 out = open(tempfn, 'wb') 

3713 else: 

3714 out = sys.stdout 

3715 

3716 if bbox and not self.is_gmt5(): 

3717 out.write(replace_bbox(bbox, self.output.getvalue())) 

3718 else: 

3719 out.write(self.output.getvalue()) 

3720 

3721 if filename: 

3722 out.close() 

3723 

3724 if filename.endswith('.ps') or ( 

3725 not self.is_gmt5() and filename.endswith('.eps')): 

3726 

3727 shutil.move(tempfn, filename) 

3728 return 

3729 

3730 if self.is_gmt5(): 

3731 if crop_eps_mode: 

3732 addarg = ['-A'] 

3733 else: 

3734 addarg = [] 

3735 

3736 subprocess.call( 

3737 [pjoin(self.installation['bin'], 'gmt'), 'psconvert', 

3738 '-Te', '-F%s' % tempfn, tempfn, ] + addarg) 

3739 

3740 if bbox: 

3741 with open(tempfn + '.eps', 'rb') as fin: 

3742 with open(tempfn + '-fixbb.eps', 'wb') as fout: 

3743 replace_bbox(bbox, fin, fout) 

3744 

3745 shutil.move(tempfn + '-fixbb.eps', tempfn + '.eps') 

3746 

3747 else: 

3748 shutil.move(tempfn, tempfn + '.eps') 

3749 

3750 if filename.endswith('.eps'): 

3751 shutil.move(tempfn + '.eps', filename) 

3752 return 

3753 

3754 elif filename.endswith('.pdf'): 

3755 if psconvert: 

3756 gmt_bin = pjoin(self.installation['bin'], 'gmt') 

3757 subprocess.call([gmt_bin, 'psconvert', tempfn + '.eps', '-Tf', 

3758 '-F' + filename]) 

3759 else: 

3760 subprocess.call(['gmtpy-epstopdf', '--res=%i' % resolution, 

3761 '--outfile=' + filename, tempfn + '.eps']) 

3762 else: 

3763 subprocess.call([ 

3764 'gmtpy-epstopdf', 

3765 '--res=%i' % (resolution * oversample), 

3766 '--outfile=' + tempfn + '.pdf', tempfn + '.eps']) 

3767 

3768 convert_graph( 

3769 tempfn + '.pdf', filename, 

3770 resolution=resolution, oversample=oversample, 

3771 size=size, width=width, height=height) 

3772 

3773 def bbox(self): 

3774 return get_bbox(self.output.getvalue()) 

3775 

3776 def get_command_log(self): 

3777 ''' 

3778 Get the command log. 

3779 ''' 

3780 

3781 return self.command_log 

3782 

3783 def __str__(self): 

3784 s = '' 

3785 for com in self.command_log: 

3786 s += com[0] + "\n " + "\n ".join(com[1:]) + "\n\n" 

3787 return s 

3788 

3789 def page_size_points(self): 

3790 ''' 

3791 Try to get paper size of output postscript file in points. 

3792 ''' 

3793 

3794 pm = paper_media(self.gmt_config).lower() 

3795 if pm.endswith('+') or pm.endswith('-'): 

3796 pm = pm[:-1] 

3797 

3798 orient = page_orientation(self.gmt_config).lower() 

3799 

3800 if pm in all_paper_sizes(): 

3801 

3802 if orient == 'portrait': 

3803 return get_paper_size(pm) 

3804 else: 

3805 return get_paper_size(pm)[1], get_paper_size(pm)[0] 

3806 

3807 m = re.match(r'custom_([0-9.]+)([cimp]?)x([0-9.]+)([cimp]?)', pm) 

3808 if m: 

3809 w, uw, h, uh = m.groups() 

3810 w, h = float(w), float(h) 

3811 if uw: 

3812 w *= _units[uw] 

3813 if uh: 

3814 h *= _units[uh] 

3815 if orient == 'portrait': 

3816 return w, h 

3817 else: 

3818 return h, w 

3819 

3820 return None, None 

3821 

3822 def default_layout(self, with_palette=False): 

3823 ''' 

3824 Get a default layout for the output page. 

3825 

3826 One of three different layouts is choosen, depending on the 

3827 `PAPER_MEDIA` setting in the GMT configuration dict. 

3828 

3829 If `PAPER_MEDIA` ends with a ``'+'`` (EPS output is selected), a 

3830 :py:class:`FrameLayout` is centered on the page, whose size is 

3831 controlled by its center widget's size plus the margins of the 

3832 :py:class:`FrameLayout`. 

3833 

3834 If `PAPER_MEDIA` indicates, that a custom page size is wanted by 

3835 starting with ``'Custom_'``, a :py:class:`FrameLayout` is used to fill 

3836 the complete page. The center widget's size is then controlled by the 

3837 page's size minus the margins of the :py:class:`FrameLayout`. 

3838 

3839 In any other case, two FrameLayouts are nested, such that the outer 

3840 layout attaches a 1 cm (printer) margin around the complete page, and 

3841 the inner FrameLayout's center widget takes up as much space as 

3842 possible under the constraint, that an aspect ratio of 1/golden_ratio 

3843 is preserved. 

3844 

3845 In any case, a reference to the innermost :py:class:`FrameLayout` 

3846 instance is returned. The top-level layout can be accessed by calling 

3847 :py:meth:`Widget.get_parent` on the returned layout. 

3848 ''' 

3849 

3850 if self.layout is None: 

3851 w, h = self.page_size_points() 

3852 

3853 if w is None or h is None: 

3854 raise GmtPyError("Can't determine page size for layout") 

3855 

3856 pm = paper_media(self.gmt_config).lower() 

3857 

3858 if with_palette: 

3859 palette_layout = GridLayout(3, 1) 

3860 spacer = palette_layout.get_widget(1, 0) 

3861 palette_widget = palette_layout.get_widget(2, 0) 

3862 spacer.set_horizontal(0.5*cm) 

3863 palette_widget.set_horizontal(0.5*cm) 

3864 

3865 if pm.endswith('+') or self.eps_mode: 

3866 outer = CenterLayout() 

3867 outer.set_policy((w, h), (0., 0.)) 

3868 inner = FrameLayout() 

3869 outer.set_widget(inner) 

3870 if with_palette: 

3871 inner.set_widget('center', palette_layout) 

3872 widget = palette_layout 

3873 else: 

3874 widget = inner.get_widget('center') 

3875 widget.set_policy((w/golden_ratio, 0.), (0., 0.), 

3876 aspect=1./golden_ratio) 

3877 mw = 3.0*cm 

3878 inner.set_fixed_margins( 

3879 mw, mw, mw/golden_ratio, mw/golden_ratio) 

3880 self.layout = inner 

3881 

3882 elif pm.startswith('custom_'): 

3883 layout = FrameLayout() 

3884 layout.set_policy((w, h), (0., 0.)) 

3885 mw = 3.0*cm 

3886 layout.set_min_margins( 

3887 mw, mw, mw/golden_ratio, mw/golden_ratio) 

3888 if with_palette: 

3889 layout.set_widget('center', palette_layout) 

3890 self.layout = layout 

3891 else: 

3892 outer = FrameLayout() 

3893 outer.set_policy((w, h), (0., 0.)) 

3894 outer.set_fixed_margins(1.*cm, 1.*cm, 1.*cm, 1.*cm) 

3895 

3896 inner = FrameLayout() 

3897 outer.set_widget('center', inner) 

3898 mw = 3.0*cm 

3899 inner.set_min_margins(mw, mw, mw/golden_ratio, mw/golden_ratio) 

3900 if with_palette: 

3901 inner.set_widget('center', palette_layout) 

3902 widget = palette_layout 

3903 else: 

3904 widget = inner.get_widget('center') 

3905 

3906 widget.set_aspect(1./golden_ratio) 

3907 

3908 self.layout = inner 

3909 

3910 return self.layout 

3911 

3912 def draw_layout(self, layout): 

3913 ''' 

3914 Use psxy to draw layout; for debugging 

3915 ''' 

3916 

3917 # corners = layout.get_corners(descend=True) 

3918 rects = num.array(layout.get_sizes(), dtype=float) 

3919 rects_wid = rects[:, 0, 0] 

3920 rects_hei = rects[:, 0, 1] 

3921 rects_center_x = rects[:, 1, 0] + rects_wid*0.5 

3922 rects_center_y = rects[:, 1, 1] + rects_hei*0.5 

3923 nrects = len(rects) 

3924 prects = (rects_center_x, rects_center_y, num.arange(nrects), 

3925 num.zeros(nrects), rects_hei, rects_wid) 

3926 

3927 # points = num.array(corners, dtype=float) 

3928 

3929 cptfile = self.tempfilename() + '.cpt' 

3930 self.makecpt( 

3931 C='ocean', 

3932 T='%g/%g/%g' % (-nrects, nrects, 1), 

3933 Z=True, 

3934 out_filename=cptfile, suppress_defaults=True) 

3935 

3936 bb = layout.bbox() 

3937 self.psxy( 

3938 in_columns=prects, 

3939 C=cptfile, 

3940 W='1p', 

3941 S='J', 

3942 R=(bb[0], bb[2], bb[1], bb[3]), 

3943 *layout.XYJ()) 

3944 

3945 

3946def simpleconf_to_ax(conf, axname): 

3947 c = {} 

3948 x = axname 

3949 for x in ('', axname): 

3950 for k in ('label', 'unit', 'scaled_unit', 'scaled_unit_factor', 

3951 'space', 'mode', 'approx_ticks', 'limits', 'masking', 'inc', 

3952 'snap'): 

3953 

3954 if x+k in conf: 

3955 c[k] = conf[x+k] 

3956 

3957 return Ax(**c) 

3958 

3959 

3960class DensityPlotDef(object): 

3961 def __init__(self, data, cpt='ocean', tension=0.7, size=(640, 480), 

3962 contour=False, method='surface', zscaler=None, **extra): 

3963 self.data = data 

3964 self.cpt = cpt 

3965 self.tension = tension 

3966 self.size = size 

3967 self.contour = contour 

3968 self.method = method 

3969 self.zscaler = zscaler 

3970 self.extra = extra 

3971 

3972 

3973class TextDef(object): 

3974 def __init__( 

3975 self, 

3976 data, 

3977 size=9, 

3978 justify='MC', 

3979 fontno=0, 

3980 offset=(0, 0), 

3981 color='black'): 

3982 

3983 self.data = data 

3984 self.size = size 

3985 self.justify = justify 

3986 self.fontno = fontno 

3987 self.offset = offset 

3988 self.color = color 

3989 

3990 

3991class Simple(object): 

3992 def __init__(self, gmtconfig=None, gmtversion='newest', **simple_config): 

3993 self.data = [] 

3994 self.symbols = [] 

3995 self.config = copy.deepcopy(simple_config) 

3996 self.gmtconfig = gmtconfig 

3997 self.density_plot_defs = [] 

3998 self.text_defs = [] 

3999 

4000 self.gmtversion = gmtversion 

4001 

4002 self.data_x = [] 

4003 self.symbols_x = [] 

4004 

4005 self.data_y = [] 

4006 self.symbols_y = [] 

4007 

4008 self.default_config = {} 

4009 self.set_defaults(width=15.*cm, 

4010 height=15.*cm / golden_ratio, 

4011 margins=(2.*cm, 2.*cm, 2.*cm, 2.*cm), 

4012 with_palette=False, 

4013 palette_offset=0.5*cm, 

4014 palette_width=None, 

4015 palette_height=None, 

4016 zlabeloffset=2*cm, 

4017 draw_layout=False) 

4018 

4019 self.setup_defaults() 

4020 self.fixate_widget_aspect = False 

4021 

4022 def setup_defaults(self): 

4023 pass 

4024 

4025 def set_defaults(self, **kwargs): 

4026 self.default_config.update(kwargs) 

4027 

4028 def plot(self, data, symbol=''): 

4029 self.data.append(data) 

4030 self.symbols.append(symbol) 

4031 

4032 def density_plot(self, data, **kwargs): 

4033 dpd = DensityPlotDef(data, **kwargs) 

4034 self.density_plot_defs.append(dpd) 

4035 

4036 def text(self, data, **kwargs): 

4037 dpd = TextDef(data, **kwargs) 

4038 self.text_defs.append(dpd) 

4039 

4040 def plot_x(self, data, symbol=''): 

4041 self.data_x.append(data) 

4042 self.symbols_x.append(symbol) 

4043 

4044 def plot_y(self, data, symbol=''): 

4045 self.data_y.append(data) 

4046 self.symbols_y.append(symbol) 

4047 

4048 def set(self, **kwargs): 

4049 self.config.update(kwargs) 

4050 

4051 def setup_base(self, conf): 

4052 w = conf.pop('width') 

4053 h = conf.pop('height') 

4054 margins = conf.pop('margins') 

4055 

4056 gmtconfig = {} 

4057 if self.gmtconfig is not None: 

4058 gmtconfig.update(self.gmtconfig) 

4059 

4060 gmt = GMT( 

4061 version=self.gmtversion, 

4062 config=gmtconfig, 

4063 config_papersize='Custom_%ix%i' % (w, h)) 

4064 

4065 layout = gmt.default_layout(with_palette=conf['with_palette']) 

4066 layout.set_min_margins(*margins) 

4067 if conf['with_palette']: 

4068 widget = layout.get_widget().get_widget(0, 0) 

4069 spacer = layout.get_widget().get_widget(1, 0) 

4070 spacer.set_horizontal(conf['palette_offset']) 

4071 palette_widget = layout.get_widget().get_widget(2, 0) 

4072 if conf['palette_width'] is not None: 

4073 palette_widget.set_horizontal(conf['palette_width']) 

4074 if conf['palette_height'] is not None: 

4075 palette_widget.set_vertical(conf['palette_height']) 

4076 widget.set_vertical(h-margins[2]-margins[3]-0.03*cm) 

4077 return gmt, layout, widget, palette_widget 

4078 else: 

4079 widget = layout.get_widget() 

4080 return gmt, layout, widget, None 

4081 

4082 def setup_projection(self, widget, scaler, conf): 

4083 pass 

4084 

4085 def setup_scaling(self, conf): 

4086 ndims = 2 

4087 if self.density_plot_defs: 

4088 ndims = 3 

4089 

4090 axes = [simpleconf_to_ax(conf, x) for x in 'xyz'[:ndims]] 

4091 

4092 data_all = [] 

4093 data_all.extend(self.data) 

4094 for dsd in self.density_plot_defs: 

4095 if dsd.zscaler is None: 

4096 data_all.append(dsd.data) 

4097 else: 

4098 data_all.append(dsd.data[:2]) 

4099 data_chopped = [ds[:ndims] for ds in data_all] 

4100 

4101 scaler = ScaleGuru(data_chopped, axes=axes[:ndims]) 

4102 

4103 self.setup_scaling_plus(scaler, axes[:ndims]) 

4104 

4105 return scaler 

4106 

4107 def setup_scaling_plus(self, scaler, axes): 

4108 pass 

4109 

4110 def setup_scaling_extra(self, scaler, conf): 

4111 

4112 scaler_x = scaler.copy() 

4113 scaler_x.data_ranges[1] = (0., 1.) 

4114 scaler_x.axes[1].mode = 'off' 

4115 

4116 scaler_y = scaler.copy() 

4117 scaler_y.data_ranges[0] = (0., 1.) 

4118 scaler_y.axes[0].mode = 'off' 

4119 

4120 return scaler_x, scaler_y 

4121 

4122 def draw_density(self, gmt, widget, scaler): 

4123 

4124 R = scaler.R() 

4125 # par = scaler.get_params() 

4126 rxyj = R + widget.XYJ() 

4127 innerticks = False 

4128 for dpd in self.density_plot_defs: 

4129 

4130 fn_cpt = gmt.tempfilename() + '.cpt' 

4131 

4132 if dpd.zscaler is not None: 

4133 s = dpd.zscaler 

4134 else: 

4135 s = scaler 

4136 

4137 gmt.makecpt(C=dpd.cpt, out_filename=fn_cpt, *s.T()) 

4138 

4139 fn_grid = gmt.tempfilename() 

4140 

4141 fn_mean = gmt.tempfilename() 

4142 

4143 if dpd.method in ('surface', 'triangulate'): 

4144 gmt.blockmean(in_columns=dpd.data, 

4145 I='%i+/%i+' % dpd.size, # noqa 

4146 out_filename=fn_mean, *R) 

4147 

4148 if dpd.method == 'surface': 

4149 gmt.surface( 

4150 in_filename=fn_mean, 

4151 T=dpd.tension, 

4152 G=fn_grid, 

4153 I='%i+/%i+' % dpd.size, # noqa 

4154 out_discard=True, 

4155 *R) 

4156 

4157 if dpd.method == 'triangulate': 

4158 gmt.triangulate( 

4159 in_filename=fn_mean, 

4160 G=fn_grid, 

4161 I='%i+/%i+' % dpd.size, # noqa 

4162 out_discard=True, 

4163 V=True, 

4164 *R) 

4165 

4166 if gmt.is_gmt5(): 

4167 gmt.grdimage(fn_grid, C=fn_cpt, E='i', n='l', *rxyj) 

4168 

4169 else: 

4170 gmt.grdimage(fn_grid, C=fn_cpt, E='i', S='l', *rxyj) 

4171 

4172 if dpd.contour: 

4173 gmt.grdcontour(fn_grid, C=fn_cpt, W='0.5p,black', *rxyj) 

4174 innerticks = '0.5p,black' 

4175 

4176 os.remove(fn_grid) 

4177 os.remove(fn_mean) 

4178 

4179 if dpd.method == 'fillcontour': 

4180 extra = dict(C=fn_cpt) 

4181 extra.update(dpd.extra) 

4182 gmt.pscontour(in_columns=dpd.data, 

4183 I=True, *rxyj, **extra) # noqa 

4184 

4185 if dpd.method == 'contour': 

4186 extra = dict(W='0.5p,black', C=fn_cpt) 

4187 extra.update(dpd.extra) 

4188 gmt.pscontour(in_columns=dpd.data, *rxyj, **extra) 

4189 

4190 return fn_cpt, innerticks 

4191 

4192 def draw_basemap(self, gmt, widget, scaler): 

4193 gmt.psbasemap(*(widget.JXY() + scaler.RB(ax_projection=True))) 

4194 

4195 def draw(self, gmt, widget, scaler): 

4196 rxyj = scaler.R() + widget.JXY() 

4197 for dat, sym in zip(self.data, self.symbols): 

4198 gmt.psxy(in_columns=dat, *(sym.split()+rxyj)) 

4199 

4200 def post_draw(self, gmt, widget, scaler): 

4201 pass 

4202 

4203 def pre_draw(self, gmt, widget, scaler): 

4204 pass 

4205 

4206 def draw_extra(self, gmt, widget, scaler_x, scaler_y): 

4207 

4208 for dat, sym in zip(self.data_x, self.symbols_x): 

4209 gmt.psxy(in_columns=dat, 

4210 *(sym.split() + scaler_x.R() + widget.JXY())) 

4211 

4212 for dat, sym in zip(self.data_y, self.symbols_y): 

4213 gmt.psxy(in_columns=dat, 

4214 *(sym.split() + scaler_y.R() + widget.JXY())) 

4215 

4216 def draw_text(self, gmt, widget, scaler): 

4217 

4218 rxyj = scaler.R() + widget.JXY() 

4219 for td in self.text_defs: 

4220 x, y = td.data[0:2] 

4221 text = td.data[-1] 

4222 size = td.size 

4223 angle = 0 

4224 fontno = td.fontno 

4225 justify = td.justify 

4226 color = td.color 

4227 if gmt.is_gmt5(): 

4228 gmt.pstext( 

4229 in_rows=[(x, y, text)], 

4230 F='+f%gp,%s,%s+a%g+j%s' % ( 

4231 size, fontno, color, angle, justify), 

4232 D='%gp/%gp' % td.offset, *rxyj) 

4233 else: 

4234 gmt.pstext( 

4235 in_rows=[(x, y, size, angle, fontno, justify, text)], 

4236 D='%gp/%gp' % td.offset, *rxyj) 

4237 

4238 def save(self, filename, resolution=150): 

4239 

4240 conf = dict(self.default_config) 

4241 conf.update(self.config) 

4242 

4243 gmt, layout, widget, palette_widget = self.setup_base(conf) 

4244 scaler = self.setup_scaling(conf) 

4245 scaler_x, scaler_y = self.setup_scaling_extra(scaler, conf) 

4246 

4247 self.setup_projection(widget, scaler, conf) 

4248 if self.fixate_widget_aspect: 

4249 aspect = aspect_for_projection( 

4250 gmt.installation['version'], *(widget.J() + scaler.R())) 

4251 

4252 widget.set_aspect(aspect) 

4253 

4254 if conf['draw_layout']: 

4255 gmt.draw_layout(layout) 

4256 cptfile = None 

4257 if self.density_plot_defs: 

4258 cptfile, innerticks = self.draw_density(gmt, widget, scaler) 

4259 self.pre_draw(gmt, widget, scaler) 

4260 self.draw(gmt, widget, scaler) 

4261 self.post_draw(gmt, widget, scaler) 

4262 self.draw_extra(gmt, widget, scaler_x, scaler_y) 

4263 self.draw_text(gmt, widget, scaler) 

4264 self.draw_basemap(gmt, widget, scaler) 

4265 

4266 if palette_widget and cptfile: 

4267 nice_palette(gmt, palette_widget, scaler, cptfile, 

4268 innerticks=innerticks, 

4269 zlabeloffset=conf['zlabeloffset']) 

4270 

4271 gmt.save(filename, resolution=resolution) 

4272 

4273 

4274class LinLinPlot(Simple): 

4275 pass 

4276 

4277 

4278class LogLinPlot(Simple): 

4279 

4280 def setup_defaults(self): 

4281 self.set_defaults(xmode='min-max') 

4282 

4283 def setup_projection(self, widget, scaler, conf): 

4284 widget['J'] = '-JX%(width)gpl/%(height)gp' 

4285 scaler['B'] = '-B2:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen' 

4286 

4287 

4288class LinLogPlot(Simple): 

4289 

4290 def setup_defaults(self): 

4291 self.set_defaults(ymode='min-max') 

4292 

4293 def setup_projection(self, widget, scaler, conf): 

4294 widget['J'] = '-JX%(width)gp/%(height)gpl' 

4295 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/2:%(ylabel)s:WSen' 

4296 

4297 

4298class LogLogPlot(Simple): 

4299 

4300 def setup_defaults(self): 

4301 self.set_defaults(mode='min-max') 

4302 

4303 def setup_projection(self, widget, scaler, conf): 

4304 widget['J'] = '-JX%(width)gpl/%(height)gpl' 

4305 scaler['B'] = '-B2:%(xlabel)s:/2:%(ylabel)s:WSen' 

4306 

4307 

4308class AziDistPlot(Simple): 

4309 

4310 def __init__(self, *args, **kwargs): 

4311 Simple.__init__(self, *args, **kwargs) 

4312 self.fixate_widget_aspect = True 

4313 

4314 def setup_defaults(self): 

4315 self.set_defaults( 

4316 height=15.*cm, 

4317 width=15.*cm, 

4318 xmode='off', 

4319 xlimits=(0., 360.), 

4320 xinc=45.) 

4321 

4322 def setup_projection(self, widget, scaler, conf): 

4323 widget['J'] = '-JPa%(width)gp' 

4324 

4325 def setup_scaling_plus(self, scaler, axes): 

4326 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/%(yinc)g:%(ylabel)s:N' 

4327 

4328 

4329class MPlot(Simple): 

4330 

4331 def __init__(self, *args, **kwargs): 

4332 Simple.__init__(self, *args, **kwargs) 

4333 self.fixate_widget_aspect = True 

4334 

4335 def setup_defaults(self): 

4336 self.set_defaults(xmode='min-max', ymode='min-max') 

4337 

4338 def setup_projection(self, widget, scaler, conf): 

4339 par = scaler.get_params() 

4340 lon0 = (par['xmin'] + par['xmax'])/2. 

4341 lat0 = (par['ymin'] + par['ymax'])/2. 

4342 sll = '%g/%g' % (lon0, lat0) 

4343 widget['J'] = '-JM' + sll + '/%(width)gp' 

4344 scaler['B'] = \ 

4345 '-B%(xinc)gg%(xinc)g:%(xlabel)s:/%(yinc)gg%(yinc)g:%(ylabel)s:WSen' 

4346 

4347 

4348def nice_palette(gmt, widget, scaleguru, cptfile, zlabeloffset=0.8*inch, 

4349 innerticks=True): 

4350 

4351 par = scaleguru.get_params() 

4352 par_ax = scaleguru.get_params(ax_projection=True) 

4353 nz_palette = int(widget.height()/inch * 300) 

4354 px = num.zeros(nz_palette*2) 

4355 px[1::2] += 1 

4356 pz = num.linspace(par['zmin'], par['zmax'], nz_palette).repeat(2) 

4357 pdz = pz[2]-pz[0] 

4358 palgrdfile = gmt.tempfilename() 

4359 pal_r = (0, 1, par['zmin'], par['zmax']) 

4360 pal_ax_r = (0, 1, par_ax['zmin'], par_ax['zmax']) 

4361 gmt.xyz2grd( 

4362 G=palgrdfile, R=pal_r, 

4363 I=(1, pdz), in_columns=(px, pz, pz), # noqa 

4364 out_discard=True) 

4365 

4366 gmt.grdimage(palgrdfile, R=pal_r, C=cptfile, *widget.JXY()) 

4367 if isinstance(innerticks, str): 

4368 tickpen = innerticks 

4369 gmt.grdcontour(palgrdfile, W=tickpen, R=pal_r, C=cptfile, 

4370 *widget.JXY()) 

4371 

4372 negpalwid = '%gp' % -widget.width() 

4373 if not isinstance(innerticks, str) and innerticks: 

4374 ticklen = negpalwid 

4375 else: 

4376 ticklen = '0p' 

4377 

4378 TICK_LENGTH_PARAM = 'MAP_TICK_LENGTH' if gmt.is_gmt5() else 'TICK_LENGTH' 

4379 gmt.psbasemap( 

4380 R=pal_ax_r, B='4::/%(zinc)g::nsw' % par_ax, 

4381 config={TICK_LENGTH_PARAM: ticklen}, 

4382 *widget.JXY()) 

4383 

4384 if innerticks: 

4385 gmt.psbasemap( 

4386 R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax, 

4387 config={TICK_LENGTH_PARAM: '0p'}, 

4388 *widget.JXY()) 

4389 else: 

4390 gmt.psbasemap(R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax, *widget.JXY()) 

4391 

4392 if par_ax['zlabel']: 

4393 label_font = gmt.label_font() 

4394 label_font_size = gmt.label_font_size() 

4395 label_offset = zlabeloffset 

4396 gmt.pstext( 

4397 R=(0, 1, 0, 2), D="%gp/0p" % label_offset, 

4398 N=True, 

4399 in_rows=[(1, 1, label_font_size, -90, label_font, 'CB', 

4400 par_ax['zlabel'])], 

4401 *widget.JXY())