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 

12import subprocess 

13try: 

14 from StringIO import StringIO as BytesIO 

15except ImportError: 

16 from io import BytesIO 

17import re 

18import os 

19import sys 

20import shutil 

21from os.path import join as pjoin 

22import tempfile 

23import random 

24import logging 

25import math 

26import numpy as num 

27import copy 

28from select import select 

29try: 

30 from scipy.io import netcdf_file 

31except ImportError: 

32 from scipy.io.netcdf import netcdf_file 

33 

34from pyrocko import ExternalProgramMissing 

35 

36 

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

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

39 

40 

41encoding_gmt_to_python = { 

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

43 'standard+': 'ascii', 

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

45 'standard': 'ascii'} 

46 

47for i in range(1, 11): 

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

49 

50 

51def have_gmt(): 

52 try: 

53 get_gmt_installation('newest') 

54 return True 

55 

56 except GMTInstallationProblem: 

57 return False 

58 

59 

60def check_have_gmt(): 

61 if not have_gmt(): 

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

63 

64 

65def have_pixmaptools(): 

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

67 try: 

68 p = subprocess.Popen( 

69 prog, 

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

71 

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

73 

74 except OSError: 

75 return False 

76 

77 return True 

78 

79 

80class GmtPyError(Exception): 

81 pass 

82 

83 

84class GMTError(GmtPyError): 

85 pass 

86 

87 

88class GMTInstallationProblem(GmtPyError): 

89 pass 

90 

91 

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

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

94 

95 _, tmp_filename_base = tempfile.mkstemp() 

96 

97 try: 

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

99 fmt_arg = '-svg' 

100 tmp_filename = tmp_filename_base 

101 oversample = 1.0 

102 else: 

103 fmt_arg = '-png' 

104 tmp_filename = tmp_filename_base + '-1.png' 

105 

106 if size is not None: 

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

108 elif width is not None: 

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

110 elif height is not None: 

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

112 else: 

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

114 

115 try: 

116 subprocess.check_call( 

117 ['pdftocairo'] + scale_args + 

118 [fmt_arg, in_filename, tmp_filename_base]) 

119 except OSError as e: 

120 raise GmtPyError( 

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

122 

123 if oversample > 1.: 

124 try: 

125 subprocess.check_call([ 

126 'convert', 

127 tmp_filename, 

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

129 out_filename]) 

130 except OSError as e: 

131 raise GmtPyError( 

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

133 

134 else: 

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

136 shutil.move(tmp_filename, out_filename) 

137 else: 

138 try: 

139 subprocess.check_call( 

140 ['convert', tmp_filename, out_filename]) 

141 except Exception as e: 

142 raise GmtPyError( 

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

144 % str(e)) 

145 

146 except Exception: 

147 raise 

148 

149 finally: 

150 if os.path.exists(tmp_filename_base): 

151 os.remove(tmp_filename_base) 

152 

153 if os.path.exists(tmp_filename): 

154 os.remove(tmp_filename) 

155 

156 

157def get_bbox(s): 

158 for pat in [find_hiresbb, find_bb]: 

159 m = pat.search(s) 

160 if m: 

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

162 return bb 

163 

164 raise GmtPyError('Cannot find bbox') 

165 

166 

167def replace_bbox(bbox, *args): 

168 

169 def repl(m): 

170 if m.group(1): 

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

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

173 else: 

174 return ('%%%%BoundingBox: %i %i %i %i' % ( 

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

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

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

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

179 

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

181 if len(args) == 1: 

182 s = args[0] 

183 return pat.sub(repl, s) 

184 

185 else: 

186 fin, fout = args 

187 nn = 0 

188 for line in fin: 

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

190 nn += n 

191 fout.write(line) 

192 if nn == 2: 

193 break 

194 

195 if nn == 2: 

196 for line in fin: 

197 fout.write(line) 

198 

199 

200def escape_shell_arg(s): 

201 ''' 

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

203 insecure. 

204 ''' 

205 

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

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

208 else: 

209 return s 

210 

211 

212def escape_shell_args(args): 

213 ''' 

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

215 insecure. 

216 ''' 

217 

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

219 

220 

221golden_ratio = 1.61803 

222 

223# units in points 

224_units = { 

225 'i': 72., 

226 'c': 72./2.54, 

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

228 'p': 1.} 

229 

230inch = _units['i'] 

231cm = _units['c'] 

232 

233# some awsome colors 

234tango_colors = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

262} 

263 

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

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

266 'butter2')] 

267 

268 

269def color(x=None): 

270 ''' 

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

272 

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

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

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

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

277 transformed into the string form which GMT expects. 

278 ''' 

279 

280 if x is None: 

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

282 

283 if isinstance(x, int): 

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

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

286 else: 

287 return '0/0/0' 

288 

289 elif isinstance(x, str): 

290 if x in tango_colors: 

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

292 else: 

293 return x 

294 

295 return '%i/%i/%i' % x 

296 

297 

298def color_tup(x=None): 

299 if x is None: 

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

301 

302 if isinstance(x, int): 

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

304 return graph_colors[x] 

305 else: 

306 return (0, 0, 0) 

307 

308 elif isinstance(x, str): 

309 if x in tango_colors: 

310 return tango_colors[x] 

311 

312 return x 

313 

314 

315_gmt_installations = {} 

316 

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

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

319 

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

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

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

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

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

325# 'bin': '/usr/bin' } 

326 

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

328 

329 

330def key_version(a): 

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

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

333 

334 

335def newest_installed_gmt_version(): 

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

337 

338 

339def all_installed_gmt_versions(): 

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

341 

342 

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

344# changed. 

345 

346_gmt_defaults_by_version = {} 

347_gmt_defaults_by_version['4.2.1'] = r''' 

348# 

349# GMT-SYSTEM 4.2.1 Defaults file 

350# 

351#-------- Plot Media Parameters ------------- 

352PAGE_COLOR = 255/255/255 

353PAGE_ORIENTATION = portrait 

354PAPER_MEDIA = a4+ 

355#-------- Basemap Annotation Parameters ------ 

356ANNOT_MIN_ANGLE = 20 

357ANNOT_MIN_SPACING = 0 

358ANNOT_FONT_PRIMARY = Helvetica 

359ANNOT_FONT_SIZE = 12p 

360ANNOT_OFFSET_PRIMARY = 0.075i 

361ANNOT_FONT_SECONDARY = Helvetica 

362ANNOT_FONT_SIZE_SECONDARY = 16p 

363ANNOT_OFFSET_SECONDARY = 0.075i 

364DEGREE_SYMBOL = ring 

365HEADER_FONT = Helvetica 

366HEADER_FONT_SIZE = 36p 

367HEADER_OFFSET = 0.1875i 

368LABEL_FONT = Helvetica 

369LABEL_FONT_SIZE = 14p 

370LABEL_OFFSET = 0.1125i 

371OBLIQUE_ANNOTATION = 1 

372PLOT_CLOCK_FORMAT = hh:mm:ss 

373PLOT_DATE_FORMAT = yyyy-mm-dd 

374PLOT_DEGREE_FORMAT = +ddd:mm:ss 

375Y_AXIS_TYPE = hor_text 

376#-------- Basemap Layout Parameters --------- 

377BASEMAP_AXES = WESN 

378BASEMAP_FRAME_RGB = 0/0/0 

379BASEMAP_TYPE = plain 

380FRAME_PEN = 1.25p 

381FRAME_WIDTH = 0.075i 

382GRID_CROSS_SIZE_PRIMARY = 0i 

383GRID_CROSS_SIZE_SECONDARY = 0i 

384GRID_PEN_PRIMARY = 0.25p 

385GRID_PEN_SECONDARY = 0.5p 

386MAP_SCALE_HEIGHT = 0.075i 

387TICK_LENGTH = 0.075i 

388POLAR_CAP = 85/90 

389TICK_PEN = 0.5p 

390X_AXIS_LENGTH = 9i 

391Y_AXIS_LENGTH = 6i 

392X_ORIGIN = 1i 

393Y_ORIGIN = 1i 

394UNIX_TIME = FALSE 

395UNIX_TIME_POS = -0.75i/-0.75i 

396#-------- Color System Parameters ----------- 

397COLOR_BACKGROUND = 0/0/0 

398COLOR_FOREGROUND = 255/255/255 

399COLOR_NAN = 128/128/128 

400COLOR_IMAGE = adobe 

401COLOR_MODEL = rgb 

402HSV_MIN_SATURATION = 1 

403HSV_MAX_SATURATION = 0.1 

404HSV_MIN_VALUE = 0.3 

405HSV_MAX_VALUE = 1 

406#-------- PostScript Parameters ------------- 

407CHAR_ENCODING = ISOLatin1+ 

408DOTS_PR_INCH = 300 

409N_COPIES = 1 

410PS_COLOR = rgb 

411PS_IMAGE_COMPRESS = none 

412PS_IMAGE_FORMAT = ascii 

413PS_LINE_CAP = round 

414PS_LINE_JOIN = miter 

415PS_MITER_LIMIT = 35 

416PS_VERBOSE = FALSE 

417GLOBAL_X_SCALE = 1 

418GLOBAL_Y_SCALE = 1 

419#-------- I/O Format Parameters ------------- 

420D_FORMAT = %lg 

421FIELD_DELIMITER = tab 

422GRIDFILE_SHORTHAND = FALSE 

423GRID_FORMAT = nf 

424INPUT_CLOCK_FORMAT = hh:mm:ss 

425INPUT_DATE_FORMAT = yyyy-mm-dd 

426IO_HEADER = FALSE 

427N_HEADER_RECS = 1 

428OUTPUT_CLOCK_FORMAT = hh:mm:ss 

429OUTPUT_DATE_FORMAT = yyyy-mm-dd 

430OUTPUT_DEGREE_FORMAT = +D 

431XY_TOGGLE = FALSE 

432#-------- Projection Parameters ------------- 

433ELLIPSOID = WGS-84 

434MAP_SCALE_FACTOR = default 

435MEASURE_UNIT = inch 

436#-------- Calendar/Time Parameters ---------- 

437TIME_FORMAT_PRIMARY = full 

438TIME_FORMAT_SECONDARY = full 

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

440TIME_IS_INTERVAL = OFF 

441TIME_INTERVAL_FRACTION = 0.5 

442TIME_LANGUAGE = us 

443TIME_SYSTEM = other 

444TIME_UNIT = d 

445TIME_WEEK_START = Sunday 

446Y2K_OFFSET_YEAR = 1950 

447#-------- Miscellaneous Parameters ---------- 

448HISTORY = TRUE 

449INTERPOLANT = akima 

450LINE_STEP = 0.01i 

451VECTOR_SHAPE = 0 

452VERBOSE = FALSE''' 

453 

454_gmt_defaults_by_version['4.3.0'] = r''' 

455# 

456# GMT-SYSTEM 4.3.0 Defaults file 

457# 

458#-------- Plot Media Parameters ------------- 

459PAGE_COLOR = 255/255/255 

460PAGE_ORIENTATION = portrait 

461PAPER_MEDIA = a4+ 

462#-------- Basemap Annotation Parameters ------ 

463ANNOT_MIN_ANGLE = 20 

464ANNOT_MIN_SPACING = 0 

465ANNOT_FONT_PRIMARY = Helvetica 

466ANNOT_FONT_SIZE_PRIMARY = 12p 

467ANNOT_OFFSET_PRIMARY = 0.075i 

468ANNOT_FONT_SECONDARY = Helvetica 

469ANNOT_FONT_SIZE_SECONDARY = 16p 

470ANNOT_OFFSET_SECONDARY = 0.075i 

471DEGREE_SYMBOL = ring 

472HEADER_FONT = Helvetica 

473HEADER_FONT_SIZE = 36p 

474HEADER_OFFSET = 0.1875i 

475LABEL_FONT = Helvetica 

476LABEL_FONT_SIZE = 14p 

477LABEL_OFFSET = 0.1125i 

478OBLIQUE_ANNOTATION = 1 

479PLOT_CLOCK_FORMAT = hh:mm:ss 

480PLOT_DATE_FORMAT = yyyy-mm-dd 

481PLOT_DEGREE_FORMAT = +ddd:mm:ss 

482Y_AXIS_TYPE = hor_text 

483#-------- Basemap Layout Parameters --------- 

484BASEMAP_AXES = WESN 

485BASEMAP_FRAME_RGB = 0/0/0 

486BASEMAP_TYPE = plain 

487FRAME_PEN = 1.25p 

488FRAME_WIDTH = 0.075i 

489GRID_CROSS_SIZE_PRIMARY = 0i 

490GRID_PEN_PRIMARY = 0.25p 

491GRID_CROSS_SIZE_SECONDARY = 0i 

492GRID_PEN_SECONDARY = 0.5p 

493MAP_SCALE_HEIGHT = 0.075i 

494POLAR_CAP = 85/90 

495TICK_LENGTH = 0.075i 

496TICK_PEN = 0.5p 

497X_AXIS_LENGTH = 9i 

498Y_AXIS_LENGTH = 6i 

499X_ORIGIN = 1i 

500Y_ORIGIN = 1i 

501UNIX_TIME = FALSE 

502UNIX_TIME_POS = BL/-0.75i/-0.75i 

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

504#-------- Color System Parameters ----------- 

505COLOR_BACKGROUND = 0/0/0 

506COLOR_FOREGROUND = 255/255/255 

507COLOR_NAN = 128/128/128 

508COLOR_IMAGE = adobe 

509COLOR_MODEL = rgb 

510HSV_MIN_SATURATION = 1 

511HSV_MAX_SATURATION = 0.1 

512HSV_MIN_VALUE = 0.3 

513HSV_MAX_VALUE = 1 

514#-------- PostScript Parameters ------------- 

515CHAR_ENCODING = ISOLatin1+ 

516DOTS_PR_INCH = 300 

517N_COPIES = 1 

518PS_COLOR = rgb 

519PS_IMAGE_COMPRESS = none 

520PS_IMAGE_FORMAT = ascii 

521PS_LINE_CAP = round 

522PS_LINE_JOIN = miter 

523PS_MITER_LIMIT = 35 

524PS_VERBOSE = FALSE 

525GLOBAL_X_SCALE = 1 

526GLOBAL_Y_SCALE = 1 

527#-------- I/O Format Parameters ------------- 

528D_FORMAT = %lg 

529FIELD_DELIMITER = tab 

530GRIDFILE_SHORTHAND = FALSE 

531GRID_FORMAT = nf 

532INPUT_CLOCK_FORMAT = hh:mm:ss 

533INPUT_DATE_FORMAT = yyyy-mm-dd 

534IO_HEADER = FALSE 

535N_HEADER_RECS = 1 

536OUTPUT_CLOCK_FORMAT = hh:mm:ss 

537OUTPUT_DATE_FORMAT = yyyy-mm-dd 

538OUTPUT_DEGREE_FORMAT = +D 

539XY_TOGGLE = FALSE 

540#-------- Projection Parameters ------------- 

541ELLIPSOID = WGS-84 

542MAP_SCALE_FACTOR = default 

543MEASURE_UNIT = inch 

544#-------- Calendar/Time Parameters ---------- 

545TIME_FORMAT_PRIMARY = full 

546TIME_FORMAT_SECONDARY = full 

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

548TIME_IS_INTERVAL = OFF 

549TIME_INTERVAL_FRACTION = 0.5 

550TIME_LANGUAGE = us 

551TIME_UNIT = d 

552TIME_WEEK_START = Sunday 

553Y2K_OFFSET_YEAR = 1950 

554#-------- Miscellaneous Parameters ---------- 

555HISTORY = TRUE 

556INTERPOLANT = akima 

557LINE_STEP = 0.01i 

558VECTOR_SHAPE = 0 

559VERBOSE = FALSE''' 

560 

561 

562_gmt_defaults_by_version['4.3.1'] = r''' 

563# 

564# GMT-SYSTEM 4.3.1 Defaults file 

565# 

566#-------- Plot Media Parameters ------------- 

567PAGE_COLOR = 255/255/255 

568PAGE_ORIENTATION = portrait 

569PAPER_MEDIA = a4+ 

570#-------- Basemap Annotation Parameters ------ 

571ANNOT_MIN_ANGLE = 20 

572ANNOT_MIN_SPACING = 0 

573ANNOT_FONT_PRIMARY = Helvetica 

574ANNOT_FONT_SIZE_PRIMARY = 12p 

575ANNOT_OFFSET_PRIMARY = 0.075i 

576ANNOT_FONT_SECONDARY = Helvetica 

577ANNOT_FONT_SIZE_SECONDARY = 16p 

578ANNOT_OFFSET_SECONDARY = 0.075i 

579DEGREE_SYMBOL = ring 

580HEADER_FONT = Helvetica 

581HEADER_FONT_SIZE = 36p 

582HEADER_OFFSET = 0.1875i 

583LABEL_FONT = Helvetica 

584LABEL_FONT_SIZE = 14p 

585LABEL_OFFSET = 0.1125i 

586OBLIQUE_ANNOTATION = 1 

587PLOT_CLOCK_FORMAT = hh:mm:ss 

588PLOT_DATE_FORMAT = yyyy-mm-dd 

589PLOT_DEGREE_FORMAT = +ddd:mm:ss 

590Y_AXIS_TYPE = hor_text 

591#-------- Basemap Layout Parameters --------- 

592BASEMAP_AXES = WESN 

593BASEMAP_FRAME_RGB = 0/0/0 

594BASEMAP_TYPE = plain 

595FRAME_PEN = 1.25p 

596FRAME_WIDTH = 0.075i 

597GRID_CROSS_SIZE_PRIMARY = 0i 

598GRID_PEN_PRIMARY = 0.25p 

599GRID_CROSS_SIZE_SECONDARY = 0i 

600GRID_PEN_SECONDARY = 0.5p 

601MAP_SCALE_HEIGHT = 0.075i 

602POLAR_CAP = 85/90 

603TICK_LENGTH = 0.075i 

604TICK_PEN = 0.5p 

605X_AXIS_LENGTH = 9i 

606Y_AXIS_LENGTH = 6i 

607X_ORIGIN = 1i 

608Y_ORIGIN = 1i 

609UNIX_TIME = FALSE 

610UNIX_TIME_POS = BL/-0.75i/-0.75i 

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

612#-------- Color System Parameters ----------- 

613COLOR_BACKGROUND = 0/0/0 

614COLOR_FOREGROUND = 255/255/255 

615COLOR_NAN = 128/128/128 

616COLOR_IMAGE = adobe 

617COLOR_MODEL = rgb 

618HSV_MIN_SATURATION = 1 

619HSV_MAX_SATURATION = 0.1 

620HSV_MIN_VALUE = 0.3 

621HSV_MAX_VALUE = 1 

622#-------- PostScript Parameters ------------- 

623CHAR_ENCODING = ISOLatin1+ 

624DOTS_PR_INCH = 300 

625N_COPIES = 1 

626PS_COLOR = rgb 

627PS_IMAGE_COMPRESS = none 

628PS_IMAGE_FORMAT = ascii 

629PS_LINE_CAP = round 

630PS_LINE_JOIN = miter 

631PS_MITER_LIMIT = 35 

632PS_VERBOSE = FALSE 

633GLOBAL_X_SCALE = 1 

634GLOBAL_Y_SCALE = 1 

635#-------- I/O Format Parameters ------------- 

636D_FORMAT = %lg 

637FIELD_DELIMITER = tab 

638GRIDFILE_SHORTHAND = FALSE 

639GRID_FORMAT = nf 

640INPUT_CLOCK_FORMAT = hh:mm:ss 

641INPUT_DATE_FORMAT = yyyy-mm-dd 

642IO_HEADER = FALSE 

643N_HEADER_RECS = 1 

644OUTPUT_CLOCK_FORMAT = hh:mm:ss 

645OUTPUT_DATE_FORMAT = yyyy-mm-dd 

646OUTPUT_DEGREE_FORMAT = +D 

647XY_TOGGLE = FALSE 

648#-------- Projection Parameters ------------- 

649ELLIPSOID = WGS-84 

650MAP_SCALE_FACTOR = default 

651MEASURE_UNIT = inch 

652#-------- Calendar/Time Parameters ---------- 

653TIME_FORMAT_PRIMARY = full 

654TIME_FORMAT_SECONDARY = full 

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

656TIME_IS_INTERVAL = OFF 

657TIME_INTERVAL_FRACTION = 0.5 

658TIME_LANGUAGE = us 

659TIME_UNIT = d 

660TIME_WEEK_START = Sunday 

661Y2K_OFFSET_YEAR = 1950 

662#-------- Miscellaneous Parameters ---------- 

663HISTORY = TRUE 

664INTERPOLANT = akima 

665LINE_STEP = 0.01i 

666VECTOR_SHAPE = 0 

667VERBOSE = FALSE''' 

668 

669 

670_gmt_defaults_by_version['4.4.0'] = r''' 

671# 

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

673# 

674#-------- Plot Media Parameters ------------- 

675PAGE_COLOR = 255/255/255 

676PAGE_ORIENTATION = portrait 

677PAPER_MEDIA = a4+ 

678#-------- Basemap Annotation Parameters ------ 

679ANNOT_MIN_ANGLE = 20 

680ANNOT_MIN_SPACING = 0 

681ANNOT_FONT_PRIMARY = Helvetica 

682ANNOT_FONT_SIZE_PRIMARY = 14p 

683ANNOT_OFFSET_PRIMARY = 0.075i 

684ANNOT_FONT_SECONDARY = Helvetica 

685ANNOT_FONT_SIZE_SECONDARY = 16p 

686ANNOT_OFFSET_SECONDARY = 0.075i 

687DEGREE_SYMBOL = ring 

688HEADER_FONT = Helvetica 

689HEADER_FONT_SIZE = 36p 

690HEADER_OFFSET = 0.1875i 

691LABEL_FONT = Helvetica 

692LABEL_FONT_SIZE = 14p 

693LABEL_OFFSET = 0.1125i 

694OBLIQUE_ANNOTATION = 1 

695PLOT_CLOCK_FORMAT = hh:mm:ss 

696PLOT_DATE_FORMAT = yyyy-mm-dd 

697PLOT_DEGREE_FORMAT = +ddd:mm:ss 

698Y_AXIS_TYPE = hor_text 

699#-------- Basemap Layout Parameters --------- 

700BASEMAP_AXES = WESN 

701BASEMAP_FRAME_RGB = 0/0/0 

702BASEMAP_TYPE = plain 

703FRAME_PEN = 1.25p 

704FRAME_WIDTH = 0.075i 

705GRID_CROSS_SIZE_PRIMARY = 0i 

706GRID_PEN_PRIMARY = 0.25p 

707GRID_CROSS_SIZE_SECONDARY = 0i 

708GRID_PEN_SECONDARY = 0.5p 

709MAP_SCALE_HEIGHT = 0.075i 

710POLAR_CAP = 85/90 

711TICK_LENGTH = 0.075i 

712TICK_PEN = 0.5p 

713X_AXIS_LENGTH = 9i 

714Y_AXIS_LENGTH = 6i 

715X_ORIGIN = 1i 

716Y_ORIGIN = 1i 

717UNIX_TIME = FALSE 

718UNIX_TIME_POS = BL/-0.75i/-0.75i 

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

720#-------- Color System Parameters ----------- 

721COLOR_BACKGROUND = 0/0/0 

722COLOR_FOREGROUND = 255/255/255 

723COLOR_NAN = 128/128/128 

724COLOR_IMAGE = adobe 

725COLOR_MODEL = rgb 

726HSV_MIN_SATURATION = 1 

727HSV_MAX_SATURATION = 0.1 

728HSV_MIN_VALUE = 0.3 

729HSV_MAX_VALUE = 1 

730#-------- PostScript Parameters ------------- 

731CHAR_ENCODING = ISOLatin1+ 

732DOTS_PR_INCH = 300 

733N_COPIES = 1 

734PS_COLOR = rgb 

735PS_IMAGE_COMPRESS = lzw 

736PS_IMAGE_FORMAT = ascii 

737PS_LINE_CAP = round 

738PS_LINE_JOIN = miter 

739PS_MITER_LIMIT = 35 

740PS_VERBOSE = FALSE 

741GLOBAL_X_SCALE = 1 

742GLOBAL_Y_SCALE = 1 

743#-------- I/O Format Parameters ------------- 

744D_FORMAT = %lg 

745FIELD_DELIMITER = tab 

746GRIDFILE_SHORTHAND = FALSE 

747GRID_FORMAT = nf 

748INPUT_CLOCK_FORMAT = hh:mm:ss 

749INPUT_DATE_FORMAT = yyyy-mm-dd 

750IO_HEADER = FALSE 

751N_HEADER_RECS = 1 

752OUTPUT_CLOCK_FORMAT = hh:mm:ss 

753OUTPUT_DATE_FORMAT = yyyy-mm-dd 

754OUTPUT_DEGREE_FORMAT = +D 

755XY_TOGGLE = FALSE 

756#-------- Projection Parameters ------------- 

757ELLIPSOID = WGS-84 

758MAP_SCALE_FACTOR = default 

759MEASURE_UNIT = inch 

760#-------- Calendar/Time Parameters ---------- 

761TIME_FORMAT_PRIMARY = full 

762TIME_FORMAT_SECONDARY = full 

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

764TIME_IS_INTERVAL = OFF 

765TIME_INTERVAL_FRACTION = 0.5 

766TIME_LANGUAGE = us 

767TIME_UNIT = d 

768TIME_WEEK_START = Sunday 

769Y2K_OFFSET_YEAR = 1950 

770#-------- Miscellaneous Parameters ---------- 

771HISTORY = TRUE 

772INTERPOLANT = akima 

773LINE_STEP = 0.01i 

774VECTOR_SHAPE = 0 

775VERBOSE = FALSE 

776''' 

777 

778_gmt_defaults_by_version['4.5.2'] = r''' 

779# 

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

781# 

782#-------- Plot Media Parameters ------------- 

783PAGE_COLOR = white 

784PAGE_ORIENTATION = portrait 

785PAPER_MEDIA = a4+ 

786#-------- Basemap Annotation Parameters ------ 

787ANNOT_MIN_ANGLE = 20 

788ANNOT_MIN_SPACING = 0 

789ANNOT_FONT_PRIMARY = Helvetica 

790ANNOT_FONT_SIZE_PRIMARY = 14p 

791ANNOT_OFFSET_PRIMARY = 0.075i 

792ANNOT_FONT_SECONDARY = Helvetica 

793ANNOT_FONT_SIZE_SECONDARY = 16p 

794ANNOT_OFFSET_SECONDARY = 0.075i 

795DEGREE_SYMBOL = ring 

796HEADER_FONT = Helvetica 

797HEADER_FONT_SIZE = 36p 

798HEADER_OFFSET = 0.1875i 

799LABEL_FONT = Helvetica 

800LABEL_FONT_SIZE = 14p 

801LABEL_OFFSET = 0.1125i 

802OBLIQUE_ANNOTATION = 1 

803PLOT_CLOCK_FORMAT = hh:mm:ss 

804PLOT_DATE_FORMAT = yyyy-mm-dd 

805PLOT_DEGREE_FORMAT = +ddd:mm:ss 

806Y_AXIS_TYPE = hor_text 

807#-------- Basemap Layout Parameters --------- 

808BASEMAP_AXES = WESN 

809BASEMAP_FRAME_RGB = black 

810BASEMAP_TYPE = plain 

811FRAME_PEN = 1.25p 

812FRAME_WIDTH = 0.075i 

813GRID_CROSS_SIZE_PRIMARY = 0i 

814GRID_PEN_PRIMARY = 0.25p 

815GRID_CROSS_SIZE_SECONDARY = 0i 

816GRID_PEN_SECONDARY = 0.5p 

817MAP_SCALE_HEIGHT = 0.075i 

818POLAR_CAP = 85/90 

819TICK_LENGTH = 0.075i 

820TICK_PEN = 0.5p 

821X_AXIS_LENGTH = 9i 

822Y_AXIS_LENGTH = 6i 

823X_ORIGIN = 1i 

824Y_ORIGIN = 1i 

825UNIX_TIME = FALSE 

826UNIX_TIME_POS = BL/-0.75i/-0.75i 

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

828#-------- Color System Parameters ----------- 

829COLOR_BACKGROUND = black 

830COLOR_FOREGROUND = white 

831COLOR_NAN = 128 

832COLOR_IMAGE = adobe 

833COLOR_MODEL = rgb 

834HSV_MIN_SATURATION = 1 

835HSV_MAX_SATURATION = 0.1 

836HSV_MIN_VALUE = 0.3 

837HSV_MAX_VALUE = 1 

838#-------- PostScript Parameters ------------- 

839CHAR_ENCODING = ISOLatin1+ 

840DOTS_PR_INCH = 300 

841GLOBAL_X_SCALE = 1 

842GLOBAL_Y_SCALE = 1 

843N_COPIES = 1 

844PS_COLOR = rgb 

845PS_IMAGE_COMPRESS = lzw 

846PS_IMAGE_FORMAT = ascii 

847PS_LINE_CAP = round 

848PS_LINE_JOIN = miter 

849PS_MITER_LIMIT = 35 

850PS_VERBOSE = FALSE 

851TRANSPARENCY = 0 

852#-------- I/O Format Parameters ------------- 

853D_FORMAT = %.12lg 

854FIELD_DELIMITER = tab 

855GRIDFILE_FORMAT = nf 

856GRIDFILE_SHORTHAND = FALSE 

857INPUT_CLOCK_FORMAT = hh:mm:ss 

858INPUT_DATE_FORMAT = yyyy-mm-dd 

859IO_HEADER = FALSE 

860N_HEADER_RECS = 1 

861NAN_RECORDS = pass 

862OUTPUT_CLOCK_FORMAT = hh:mm:ss 

863OUTPUT_DATE_FORMAT = yyyy-mm-dd 

864OUTPUT_DEGREE_FORMAT = D 

865XY_TOGGLE = FALSE 

866#-------- Projection Parameters ------------- 

867ELLIPSOID = WGS-84 

868MAP_SCALE_FACTOR = default 

869MEASURE_UNIT = inch 

870#-------- Calendar/Time Parameters ---------- 

871TIME_FORMAT_PRIMARY = full 

872TIME_FORMAT_SECONDARY = full 

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

874TIME_IS_INTERVAL = OFF 

875TIME_INTERVAL_FRACTION = 0.5 

876TIME_LANGUAGE = us 

877TIME_UNIT = d 

878TIME_WEEK_START = Sunday 

879Y2K_OFFSET_YEAR = 1950 

880#-------- Miscellaneous Parameters ---------- 

881HISTORY = TRUE 

882INTERPOLANT = akima 

883LINE_STEP = 0.01i 

884VECTOR_SHAPE = 0 

885VERBOSE = FALSE 

886''' 

887 

888_gmt_defaults_by_version['4.5.3'] = r''' 

889# 

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

891# 

892#-------- Plot Media Parameters ------------- 

893PAGE_COLOR = white 

894PAGE_ORIENTATION = portrait 

895PAPER_MEDIA = a4+ 

896#-------- Basemap Annotation Parameters ------ 

897ANNOT_MIN_ANGLE = 20 

898ANNOT_MIN_SPACING = 0 

899ANNOT_FONT_PRIMARY = Helvetica 

900ANNOT_FONT_SIZE_PRIMARY = 14p 

901ANNOT_OFFSET_PRIMARY = 0.075i 

902ANNOT_FONT_SECONDARY = Helvetica 

903ANNOT_FONT_SIZE_SECONDARY = 16p 

904ANNOT_OFFSET_SECONDARY = 0.075i 

905DEGREE_SYMBOL = ring 

906HEADER_FONT = Helvetica 

907HEADER_FONT_SIZE = 36p 

908HEADER_OFFSET = 0.1875i 

909LABEL_FONT = Helvetica 

910LABEL_FONT_SIZE = 14p 

911LABEL_OFFSET = 0.1125i 

912OBLIQUE_ANNOTATION = 1 

913PLOT_CLOCK_FORMAT = hh:mm:ss 

914PLOT_DATE_FORMAT = yyyy-mm-dd 

915PLOT_DEGREE_FORMAT = +ddd:mm:ss 

916Y_AXIS_TYPE = hor_text 

917#-------- Basemap Layout Parameters --------- 

918BASEMAP_AXES = WESN 

919BASEMAP_FRAME_RGB = black 

920BASEMAP_TYPE = plain 

921FRAME_PEN = 1.25p 

922FRAME_WIDTH = 0.075i 

923GRID_CROSS_SIZE_PRIMARY = 0i 

924GRID_PEN_PRIMARY = 0.25p 

925GRID_CROSS_SIZE_SECONDARY = 0i 

926GRID_PEN_SECONDARY = 0.5p 

927MAP_SCALE_HEIGHT = 0.075i 

928POLAR_CAP = 85/90 

929TICK_LENGTH = 0.075i 

930TICK_PEN = 0.5p 

931X_AXIS_LENGTH = 9i 

932Y_AXIS_LENGTH = 6i 

933X_ORIGIN = 1i 

934Y_ORIGIN = 1i 

935UNIX_TIME = FALSE 

936UNIX_TIME_POS = BL/-0.75i/-0.75i 

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

938#-------- Color System Parameters ----------- 

939COLOR_BACKGROUND = black 

940COLOR_FOREGROUND = white 

941COLOR_NAN = 128 

942COLOR_IMAGE = adobe 

943COLOR_MODEL = rgb 

944HSV_MIN_SATURATION = 1 

945HSV_MAX_SATURATION = 0.1 

946HSV_MIN_VALUE = 0.3 

947HSV_MAX_VALUE = 1 

948#-------- PostScript Parameters ------------- 

949CHAR_ENCODING = ISOLatin1+ 

950DOTS_PR_INCH = 300 

951GLOBAL_X_SCALE = 1 

952GLOBAL_Y_SCALE = 1 

953N_COPIES = 1 

954PS_COLOR = rgb 

955PS_IMAGE_COMPRESS = lzw 

956PS_IMAGE_FORMAT = ascii 

957PS_LINE_CAP = round 

958PS_LINE_JOIN = miter 

959PS_MITER_LIMIT = 35 

960PS_VERBOSE = FALSE 

961TRANSPARENCY = 0 

962#-------- I/O Format Parameters ------------- 

963D_FORMAT = %.12lg 

964FIELD_DELIMITER = tab 

965GRIDFILE_FORMAT = nf 

966GRIDFILE_SHORTHAND = FALSE 

967INPUT_CLOCK_FORMAT = hh:mm:ss 

968INPUT_DATE_FORMAT = yyyy-mm-dd 

969IO_HEADER = FALSE 

970N_HEADER_RECS = 1 

971NAN_RECORDS = pass 

972OUTPUT_CLOCK_FORMAT = hh:mm:ss 

973OUTPUT_DATE_FORMAT = yyyy-mm-dd 

974OUTPUT_DEGREE_FORMAT = D 

975XY_TOGGLE = FALSE 

976#-------- Projection Parameters ------------- 

977ELLIPSOID = WGS-84 

978MAP_SCALE_FACTOR = default 

979MEASURE_UNIT = inch 

980#-------- Calendar/Time Parameters ---------- 

981TIME_FORMAT_PRIMARY = full 

982TIME_FORMAT_SECONDARY = full 

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

984TIME_IS_INTERVAL = OFF 

985TIME_INTERVAL_FRACTION = 0.5 

986TIME_LANGUAGE = us 

987TIME_UNIT = d 

988TIME_WEEK_START = Sunday 

989Y2K_OFFSET_YEAR = 1950 

990#-------- Miscellaneous Parameters ---------- 

991HISTORY = TRUE 

992INTERPOLANT = akima 

993LINE_STEP = 0.01i 

994VECTOR_SHAPE = 0 

995VERBOSE = FALSE 

996''' 

997 

998_gmt_defaults_by_version['5.1.2'] = r''' 

999# 

1000# GMT 5.1.2 Defaults file 

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

1002# $Revision: 13836 $ 

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

1004# 

1005# COLOR Parameters 

1006# 

1007COLOR_BACKGROUND = black 

1008COLOR_FOREGROUND = white 

1009COLOR_NAN = 127.5 

1010COLOR_MODEL = none 

1011COLOR_HSV_MIN_S = 1 

1012COLOR_HSV_MAX_S = 0.1 

1013COLOR_HSV_MIN_V = 0.3 

1014COLOR_HSV_MAX_V = 1 

1015# 

1016# DIR Parameters 

1017# 

1018DIR_DATA = 

1019DIR_DCW = 

1020DIR_GSHHG = 

1021# 

1022# FONT Parameters 

1023# 

1024FONT_ANNOT_PRIMARY = 14p,Helvetica,black 

1025FONT_ANNOT_SECONDARY = 16p,Helvetica,black 

1026FONT_LABEL = 14p,Helvetica,black 

1027FONT_LOGO = 8p,Helvetica,black 

1028FONT_TITLE = 24p,Helvetica,black 

1029# 

1030# FORMAT Parameters 

1031# 

1032FORMAT_CLOCK_IN = hh:mm:ss 

1033FORMAT_CLOCK_OUT = hh:mm:ss 

1034FORMAT_CLOCK_MAP = hh:mm:ss 

1035FORMAT_DATE_IN = yyyy-mm-dd 

1036FORMAT_DATE_OUT = yyyy-mm-dd 

1037FORMAT_DATE_MAP = yyyy-mm-dd 

1038FORMAT_GEO_OUT = D 

1039FORMAT_GEO_MAP = ddd:mm:ss 

1040FORMAT_FLOAT_OUT = %.12g 

1041FORMAT_FLOAT_MAP = %.12g 

1042FORMAT_TIME_PRIMARY_MAP = full 

1043FORMAT_TIME_SECONDARY_MAP = full 

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

1045# 

1046# GMT Miscellaneous Parameters 

1047# 

1048GMT_COMPATIBILITY = 4 

1049GMT_CUSTOM_LIBS = 

1050GMT_EXTRAPOLATE_VAL = NaN 

1051GMT_FFT = auto 

1052GMT_HISTORY = true 

1053GMT_INTERPOLANT = akima 

1054GMT_TRIANGULATE = Shewchuk 

1055GMT_VERBOSE = compat 

1056GMT_LANGUAGE = us 

1057# 

1058# I/O Parameters 

1059# 

1060IO_COL_SEPARATOR = tab 

1061IO_GRIDFILE_FORMAT = nf 

1062IO_GRIDFILE_SHORTHAND = false 

1063IO_HEADER = false 

1064IO_N_HEADER_RECS = 0 

1065IO_NAN_RECORDS = pass 

1066IO_NC4_CHUNK_SIZE = auto 

1067IO_NC4_DEFLATION_LEVEL = 3 

1068IO_LONLAT_TOGGLE = false 

1069IO_SEGMENT_MARKER = > 

1070# 

1071# MAP Parameters 

1072# 

1073MAP_ANNOT_MIN_ANGLE = 20 

1074MAP_ANNOT_MIN_SPACING = 0p 

1075MAP_ANNOT_OBLIQUE = 1 

1076MAP_ANNOT_OFFSET_PRIMARY = 0.075i 

1077MAP_ANNOT_OFFSET_SECONDARY = 0.075i 

1078MAP_ANNOT_ORTHO = we 

1079MAP_DEFAULT_PEN = default,black 

1080MAP_DEGREE_SYMBOL = ring 

1081MAP_FRAME_AXES = WESNZ 

1082MAP_FRAME_PEN = thicker,black 

1083MAP_FRAME_TYPE = fancy 

1084MAP_FRAME_WIDTH = 5p 

1085MAP_GRID_CROSS_SIZE_PRIMARY = 0p 

1086MAP_GRID_CROSS_SIZE_SECONDARY = 0p 

1087MAP_GRID_PEN_PRIMARY = default,black 

1088MAP_GRID_PEN_SECONDARY = thinner,black 

1089MAP_LABEL_OFFSET = 0.1944i 

1090MAP_LINE_STEP = 0.75p 

1091MAP_LOGO = false 

1092MAP_LOGO_POS = BL/-54p/-54p 

1093MAP_ORIGIN_X = 1i 

1094MAP_ORIGIN_Y = 1i 

1095MAP_POLAR_CAP = 85/90 

1096MAP_SCALE_HEIGHT = 5p 

1097MAP_TICK_LENGTH_PRIMARY = 5p/2.5p 

1098MAP_TICK_LENGTH_SECONDARY = 15p/3.75p 

1099MAP_TICK_PEN_PRIMARY = thinner,black 

1100MAP_TICK_PEN_SECONDARY = thinner,black 

1101MAP_TITLE_OFFSET = 14p 

1102MAP_VECTOR_SHAPE = 0 

1103# 

1104# Projection Parameters 

1105# 

1106PROJ_AUX_LATITUDE = authalic 

1107PROJ_ELLIPSOID = WGS-84 

1108PROJ_LENGTH_UNIT = cm 

1109PROJ_MEAN_RADIUS = authalic 

1110PROJ_SCALE_FACTOR = default 

1111# 

1112# PostScript Parameters 

1113# 

1114PS_CHAR_ENCODING = ISOLatin1+ 

1115PS_COLOR_MODEL = rgb 

1116PS_COMMENTS = false 

1117PS_IMAGE_COMPRESS = deflate,5 

1118PS_LINE_CAP = butt 

1119PS_LINE_JOIN = miter 

1120PS_MITER_LIMIT = 35 

1121PS_MEDIA = a4 

1122PS_PAGE_COLOR = white 

1123PS_PAGE_ORIENTATION = portrait 

1124PS_SCALE_X = 1 

1125PS_SCALE_Y = 1 

1126PS_TRANSPARENCY = Normal 

1127# 

1128# Calendar/Time Parameters 

1129# 

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

1131TIME_IS_INTERVAL = off 

1132TIME_INTERVAL_FRACTION = 0.5 

1133TIME_UNIT = s 

1134TIME_WEEK_START = Monday 

1135TIME_Y2K_OFFSET_YEAR = 1950 

1136''' 

1137 

1138 

1139def get_gmt_version(gmtdefaultsbinary, gmthomedir=None): 

1140 args = [gmtdefaultsbinary] 

1141 

1142 environ = os.environ.copy() 

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

1144 

1145 p = subprocess.Popen( 

1146 args, 

1147 stdout=subprocess.PIPE, 

1148 stderr=subprocess.PIPE, 

1149 env=environ) 

1150 

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

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

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

1154 

1155 if not m: 

1156 raise GMTInstallationProblem( 

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

1158 % gmtdefaultsbinary) 

1159 

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

1161 

1162 

1163def detect_gmt_installations(): 

1164 

1165 installations = {} 

1166 errmesses = [] 

1167 

1168 # GMT 4.x: 

1169 try: 

1170 p = subprocess.Popen( 

1171 ['GMT'], 

1172 stdout=subprocess.PIPE, 

1173 stderr=subprocess.PIPE) 

1174 

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

1176 

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

1178 if not m: 

1179 raise GMTInstallationProblem( 

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

1181 

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

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

1184 

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

1186 if not m: 

1187 raise GMTInstallationProblem( 

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

1189 

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

1191 

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

1193 if not m: 

1194 raise GMTInstallationProblem( 

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

1196 

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

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

1199 raise GMTInstallationProblem( 

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

1201 

1202 gmthome = gmtshare[:-6] 

1203 

1204 installations[version] = { 

1205 'home': gmthome, 

1206 'bin': gmtbin} 

1207 

1208 except OSError as e: 

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

1210 

1211 try: 

1212 version = str(subprocess.check_output( 

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

1214 gmtbin = str(subprocess.check_output( 

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

1216 installations[version] = { 

1217 'bin': gmtbin} 

1218 

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

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

1221 

1222 if not installations: 

1223 s = [] 

1224 for (progname, errmess) in errmesses: 

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

1226 

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

1228 

1229 return installations 

1230 

1231 

1232def appropriate_defaults_version(version): 

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

1234 for iavail, avail in enumerate(avails): 

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

1236 return version 

1237 

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

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

1240 

1241 return avails[-1] 

1242 

1243 

1244def gmt_default_config(version): 

1245 ''' 

1246 Get default GMT configuration dict for given version. 

1247 ''' 

1248 

1249 xversion = appropriate_defaults_version(version) 

1250 

1251 # if not version in _gmt_defaults_by_version: 

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

1253 

1254 gmt_defaults = _gmt_defaults_by_version[xversion] 

1255 

1256 d = {} 

1257 for line in gmt_defaults.splitlines(): 

1258 sline = line.strip() 

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

1260 continue 

1261 

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

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

1264 

1265 return d 

1266 

1267 

1268def diff_defaults(v1, v2): 

1269 d1 = gmt_default_config(v1) 

1270 d2 = gmt_default_config(v2) 

1271 for k in d1: 

1272 if k not in d2: 

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

1274 else: 

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

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

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

1278 

1279 for k in d2: 

1280 if k not in d1: 

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

1282 

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

1284 

1285 

1286def check_gmt_installation(installation): 

1287 

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

1289 bin_dir = installation['bin'] 

1290 version = installation['version'] 

1291 

1292 for d in home_dir, bin_dir: 

1293 if d is not None: 

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

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

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

1297 

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

1299 

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

1301 gmtdefaults = pjoin(bin_dir, 'gmtdefaults') 

1302 

1303 versionfound = get_gmt_version(gmtdefaults, home_dir) 

1304 

1305 if versionfound != version: 

1306 raise GMTInstallationProblem(( 

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

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

1309 version, versionfound, gmtdefaults)) 

1310 

1311 

1312def get_gmt_installation(version): 

1313 setup_gmt_installations() 

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

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

1316 % (version, newest_installed_gmt_version())) 

1317 

1318 version = 'newest' 

1319 

1320 if version == 'newest': 

1321 version = newest_installed_gmt_version() 

1322 

1323 installation = dict(_gmt_installations[version]) 

1324 

1325 return installation 

1326 

1327 

1328def setup_gmt_installations(): 

1329 if not setup_gmt_installations.have_done: 

1330 if not _gmt_installations: 

1331 

1332 _gmt_installations.update(detect_gmt_installations()) 

1333 

1334 # store defaults as dicts into the gmt installations dicts 

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

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

1337 installation['version'] = version 

1338 

1339 for installation in _gmt_installations.values(): 

1340 check_gmt_installation(installation) 

1341 

1342 setup_gmt_installations.have_done = True 

1343 

1344 

1345setup_gmt_installations.have_done = False 

1346 

1347_paper_sizes_a = '''A0 2380 3368 

1348 A1 1684 2380 

1349 A2 1190 1684 

1350 A3 842 1190 

1351 A4 595 842 

1352 A5 421 595 

1353 A6 297 421 

1354 A7 210 297 

1355 A8 148 210 

1356 A9 105 148 

1357 A10 74 105 

1358 B0 2836 4008 

1359 B1 2004 2836 

1360 B2 1418 2004 

1361 B3 1002 1418 

1362 B4 709 1002 

1363 B5 501 709 

1364 archA 648 864 

1365 archB 864 1296 

1366 archC 1296 1728 

1367 archD 1728 2592 

1368 archE 2592 3456 

1369 flsa 612 936 

1370 halfletter 396 612 

1371 note 540 720 

1372 letter 612 792 

1373 legal 612 1008 

1374 11x17 792 1224 

1375 ledger 1224 792''' 

1376 

1377 

1378_paper_sizes = {} 

1379 

1380 

1381def setup_paper_sizes(): 

1382 if not _paper_sizes: 

1383 for line in _paper_sizes_a.splitlines(): 

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

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

1386 

1387 

1388def get_paper_size(k): 

1389 setup_paper_sizes() 

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

1391 

1392 

1393def all_paper_sizes(): 

1394 setup_paper_sizes() 

1395 return _paper_sizes 

1396 

1397 

1398def measure_unit(gmt_config): 

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

1400 if k in gmt_config: 

1401 return gmt_config[k] 

1402 

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

1404 

1405 

1406def paper_media(gmt_config): 

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

1408 if k in gmt_config: 

1409 return gmt_config[k] 

1410 

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

1412 

1413 

1414def page_orientation(gmt_config): 

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

1416 if k in gmt_config: 

1417 return gmt_config[k] 

1418 

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

1420 

1421 

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

1423 

1424 leftmargin, topmargin, rightmargin, bottommargin = margins 

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

1426 

1427 paper_size = get_paper_size(paper_media(gmt_config)) 

1428 if not portrait: 

1429 paper_size = paper_size[1], paper_size[0] 

1430 

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

1432 2.0 + leftmargin 

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

1434 2.0 + bottommargin 

1435 

1436 if portrait: 

1437 bb1 = int((xoffset - leftmargin)) 

1438 bb2 = int((yoffset - bottommargin)) 

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

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

1441 else: 

1442 bb1 = int((yoffset - topmargin)) 

1443 bb2 = int((xoffset - leftmargin)) 

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

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

1446 

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

1448 

1449 

1450def gmtdefaults_as_text(version='newest'): 

1451 

1452 ''' 

1453 Get the built-in gmtdefaults. 

1454 ''' 

1455 

1456 if version not in _gmt_installations: 

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

1458 % (version, newest_installed_gmt_version())) 

1459 version = 'newest' 

1460 

1461 if version == 'newest': 

1462 version = newest_installed_gmt_version() 

1463 

1464 return _gmt_defaults_by_version[version] 

1465 

1466 

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

1468 ''' 

1469 Write COARDS compliant netcdf (grd) file. 

1470 ''' 

1471 

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

1473 ny, nx = z.shape 

1474 nc = netcdf_file(filename, 'w') 

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

1476 

1477 if naming == 'xy': 

1478 kx, ky = 'x', 'y' 

1479 else: 

1480 kx, ky = 'lon', 'lat' 

1481 

1482 nc.node_offset = 0 

1483 if title is not None: 

1484 nc.title = title 

1485 

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

1487 nc.createDimension(kx, nx) 

1488 nc.createDimension(ky, ny) 

1489 

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

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

1492 if naming == 'xy': 

1493 xvar.long_name = kx 

1494 yvar.long_name = ky 

1495 else: 

1496 xvar.long_name = 'longitude' 

1497 xvar.units = 'degrees_east' 

1498 yvar.long_name = 'latitude' 

1499 yvar.units = 'degrees_north' 

1500 

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

1502 

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

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

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

1506 

1507 nc.close() 

1508 

1509 

1510def to_array(var): 

1511 arr = var[:].copy() 

1512 if hasattr(var, 'scale_factor'): 

1513 arr *= var.scale_factor 

1514 

1515 if hasattr(var, 'add_offset'): 

1516 arr += var.add_offset 

1517 

1518 return arr 

1519 

1520 

1521def loadgrd(filename): 

1522 ''' 

1523 Read COARDS compliant netcdf (grd) file. 

1524 ''' 

1525 

1526 nc = netcdf_file(filename, 'r') 

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

1528 kx = 'x' 

1529 ky = 'y' 

1530 if 'lon' in vkeys: 

1531 kx = 'lon' 

1532 if 'lat' in vkeys: 

1533 ky = 'lat' 

1534 

1535 kz = 'z' 

1536 if 'altitude' in vkeys: 

1537 kz = 'altitude' 

1538 

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

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

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

1542 

1543 nc.close() 

1544 return x, y, z 

1545 

1546 

1547def centers_to_edges(asorted): 

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

1549 

1550 

1551def nvals(asorted): 

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

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

1554 

1555 

1556def guess_vals(asorted): 

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

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

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

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

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

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

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

1564 

1565 

1566def blockmean(asorted, b): 

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

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

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

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

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

1572 return ( 

1573 asorted[indis[:-1]], 

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

1575 

1576 

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

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

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

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

1581 

1582 zindi = yindi*nx+xindi 

1583 order = num.argsort(zindi) 

1584 z = z[order] 

1585 zindi = zindi[order] 

1586 

1587 zindi, z = blockmean(zindi, z) 

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

1589 znew[:] = num.nan 

1590 znew[zindi] = z 

1591 return znew.reshape(ny, nx) 

1592 

1593 

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

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

1596 xs = x_sorted 

1597 ys = y_sorted 

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

1599 if mode == 'nonrandom': 

1600 return nxs, nys, 0 

1601 elif xs.size == nxs*nys: 

1602 # exact match 

1603 return nxs, nys, 0 

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

1605 # possibly randomly sampled 

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

1607 nys = nxs 

1608 return nxs, nys, 2 

1609 else: 

1610 return nxs, nys, 1 

1611 

1612 

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

1614 ''' 

1615 Grid tabular XYZ data by binning. 

1616 

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

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

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

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

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

1622 

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

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

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

1626 ''' 

1627 

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

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

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

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

1632 if badness <= 1: 

1633 xf = guess_vals(xs) 

1634 yf = guess_vals(ys) 

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

1636 else: 

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

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

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

1640 

1641 return xf, yf, zf 

1642 

1643 

1644def tabledata(xf, yf, zf): 

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

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

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

1648 z = zf.flatten() 

1649 return x, y, z 

1650 

1651 

1652def double1d(a): 

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

1654 a2[::2] = a 

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

1656 return a2 

1657 

1658 

1659def double2d(f): 

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

1661 f2[:, :] = num.nan 

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

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

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

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

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

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

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

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

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

1671 return f2 

1672 

1673 

1674def doublegrid(x, y, z): 

1675 x2 = double1d(x) 

1676 y2 = double1d(y) 

1677 z2 = double2d(z) 

1678 return x2, y2, z2 

1679 

1680 

1681class Guru(object): 

1682 ''' 

1683 Abstract base class providing template interpolation, accessible as 

1684 attributes. 

1685 

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

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

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

1689 with the templates. 

1690 ''' 

1691 

1692 def __init__(self): 

1693 self.templates = {} 

1694 

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

1696 params = self.get_params(**kwargs) 

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

1698 return strings 

1699 

1700 # hand through templates dict 

1701 def __getitem__(self, template_name): 

1702 return self.templates[template_name] 

1703 

1704 def __setitem__(self, template_name, template): 

1705 self.templates[template_name] = template 

1706 

1707 def __contains__(self, template_name): 

1708 return template_name in self.templates 

1709 

1710 def __iter__(self): 

1711 return iter(self.templates) 

1712 

1713 def __len__(self): 

1714 return len(self.templates) 

1715 

1716 def __delitem__(self, template_name): 

1717 del self.templates[template_name] 

1718 

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

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

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

1722 

1723 def __getattr__(self, template_names): 

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

1725 raise AttributeError(template_names) 

1726 

1727 def f(**kwargs): 

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

1729 

1730 return f 

1731 

1732 

1733def nice_value(x): 

1734 ''' 

1735 Round ``x`` to nice value. 

1736 ''' 

1737 

1738 exp = 1.0 

1739 sign = 1 

1740 if x < 0.0: 

1741 x = -x 

1742 sign = -1 

1743 while x >= 1.0: 

1744 x /= 10.0 

1745 exp *= 10.0 

1746 while x < 0.1: 

1747 x *= 10.0 

1748 exp /= 10.0 

1749 

1750 if x >= 0.75: 

1751 return sign * 1.0 * exp 

1752 if x >= 0.375: 

1753 return sign * 0.5 * exp 

1754 if x >= 0.225: 

1755 return sign * 0.25 * exp 

1756 if x >= 0.15: 

1757 return sign * 0.2 * exp 

1758 

1759 return sign * 0.1 * exp 

1760 

1761 

1762class AutoScaler(object): 

1763 ''' 

1764 Tunable 1D autoscaling based on data range. 

1765 

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

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

1768 notation. 

1769 

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

1771 

1772 .. py:attribute:: approx_ticks 

1773 

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

1775 

1776 .. py:attribute:: mode 

1777 

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

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

1780 

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

1782 mode description 

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

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

1785 below. 

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

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

1788 max. 

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

1790 zero. 

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

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

1793 disabled, such that the output range always 

1794 exactly matches the data range. 

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

1796 

1797 .. py:attribute:: exp 

1798 

1799 If defined, override automatically determined exponent for notation 

1800 by the given value. 

1801 

1802 .. py:attribute:: snap 

1803 

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

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

1806 

1807 .. py:attribute:: inc 

1808 

1809 If defined, override automatically determined tick increment by the 

1810 given value. 

1811 

1812 .. py:attribute:: space 

1813 

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

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

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

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

1818 

1819 .. py:attribute:: exp_factor 

1820 

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

1822 

1823 .. py:attribute:: no_exp_interval: 

1824 

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

1826 

1827 ''' 

1828 

1829 def __init__( 

1830 self, 

1831 approx_ticks=7.0, 

1832 mode='auto', 

1833 exp=None, 

1834 snap=False, 

1835 inc=None, 

1836 space=0.0, 

1837 exp_factor=3, 

1838 no_exp_interval=(-3, 5)): 

1839 

1840 ''' 

1841 Create new AutoScaler instance. 

1842 

1843 The parameters are described in the AutoScaler documentation. 

1844 ''' 

1845 

1846 self.approx_ticks = approx_ticks 

1847 self.mode = mode 

1848 self.exp = exp 

1849 self.snap = snap 

1850 self.inc = inc 

1851 self.space = space 

1852 self.exp_factor = exp_factor 

1853 self.no_exp_interval = no_exp_interval 

1854 

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

1856 

1857 ''' 

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

1859 

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

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

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

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

1864 value. 

1865 ''' 

1866 

1867 data_min = min(data_range) 

1868 data_max = max(data_range) 

1869 

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

1871 

1872 a = self.mode 

1873 if self.mode == 'auto': 

1874 a = self.guess_autoscale_mode(data_min, data_max) 

1875 

1876 if override_mode is not None: 

1877 a = override_mode 

1878 

1879 mi, ma = 0, 0 

1880 if a == 'off': 

1881 mi, ma = data_min, data_max 

1882 elif a == '0-max': 

1883 mi = 0.0 

1884 if data_max > 0.0: 

1885 ma = data_max 

1886 else: 

1887 ma = 1.0 

1888 elif a == 'min-0': 

1889 ma = 0.0 

1890 if data_min < 0.0: 

1891 mi = data_min 

1892 else: 

1893 mi = -1.0 

1894 elif a == 'min-max': 

1895 mi, ma = data_min, data_max 

1896 elif a == 'symmetric': 

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

1898 mi = -m 

1899 ma = m 

1900 

1901 nmi = mi 

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

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

1904 

1905 nma = ma 

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

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

1908 

1909 mi, ma = nmi, nma 

1910 

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

1912 mi -= 1.0 

1913 ma += 1.0 

1914 

1915 # make nice tick increment 

1916 if self.inc is not None: 

1917 inc = self.inc 

1918 else: 

1919 if self.approx_ticks > 0.: 

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

1921 else: 

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

1923 

1924 if inc == 0.0: 

1925 inc = 1.0 

1926 

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

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

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

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

1931 

1932 if is_reverse: 

1933 return ma, mi, -inc 

1934 else: 

1935 return mi, ma, inc 

1936 

1937 def make_exp(self, x): 

1938 ''' 

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

1940 

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

1942 ''' 

1943 

1944 if self.exp is not None: 

1945 return self.exp 

1946 

1947 x = abs(x) 

1948 if x == 0.0: 

1949 return 0 

1950 

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

1952 return 0 

1953 

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

1955 

1956 def guess_autoscale_mode(self, data_min, data_max): 

1957 ''' 

1958 Guess mode of operation, based on data range. 

1959 

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

1961 or ``'symmetric'``. 

1962 ''' 

1963 

1964 a = 'min-max' 

1965 if data_min >= 0.0: 

1966 if data_min < data_max/2.: 

1967 a = '0-max' 

1968 else: 

1969 a = 'min-max' 

1970 if data_max <= 0.0: 

1971 if data_max > data_min/2.: 

1972 a = 'min-0' 

1973 else: 

1974 a = 'min-max' 

1975 if data_min < 0.0 and data_max > 0.0: 

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

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

1978 a = 'symmetric' 

1979 else: 

1980 a = 'min-max' 

1981 return a 

1982 

1983 

1984class Ax(AutoScaler): 

1985 ''' 

1986 Ax description with autoscaling capabilities. 

1987 

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

1989 the following additional attributes (with default values given in 

1990 paranthesis): 

1991 

1992 .. py:attribute:: label 

1993 

1994 Ax label (without unit). 

1995 

1996 .. py:attribute:: unit 

1997 

1998 Physical unit of the data attached to this ax. 

1999 

2000 .. py:attribute:: scaled_unit 

2001 

2002 (see below) 

2003 

2004 .. py:attribute:: scaled_unit_factor 

2005 

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

2007 

2008 unit = scaled_unit_factor x scaled_unit. 

2009 

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

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

2012 1e9.) 

2013 

2014 .. py:attribute:: limits 

2015 

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

2017 

2018 .. py:attribute:: masking 

2019 

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

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

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

2023 

2024 ''' 

2025 

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

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

2028 

2029 AutoScaler.__init__(self, **kwargs) 

2030 self.label = label 

2031 self.unit = unit 

2032 self.scaled_unit_factor = scaled_unit_factor 

2033 self.scaled_unit = scaled_unit 

2034 self.limits = limits 

2035 self.masking = masking 

2036 

2037 def label_str(self, exp, unit): 

2038 ''' 

2039 Get label string including the unit and multiplier. 

2040 ''' 

2041 

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

2043 if self.label: 

2044 slabel = self.label 

2045 

2046 if unit or exp != 0: 

2047 if exp != 0: 

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

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

2050 else: 

2051 sunit = '[ %s ]' % unit 

2052 

2053 p = [] 

2054 if slabel: 

2055 p.append(slabel) 

2056 

2057 if sunit: 

2058 p.append(sunit) 

2059 

2060 return ' '.join(p) 

2061 

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

2063 override_scaled_unit_factor=None): 

2064 

2065 ''' 

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

2067 

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

2069 multiplier for given data range. 

2070 

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

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

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

2074 scaling applied. 

2075 ''' 

2076 

2077 sf = self.scaled_unit_factor 

2078 

2079 if override_scaled_unit_factor is not None: 

2080 sf = override_scaled_unit_factor 

2081 

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

2083 

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

2085 if self.inc is not None: 

2086 inc = self.inc*sf 

2087 

2088 if ax_projection: 

2089 exp = self.make_exp(inc) 

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

2091 unit = self.unit 

2092 else: 

2093 unit = self.scaled_unit 

2094 label = self.label_str(exp, unit) 

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

2096 else: 

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

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

2099 

2100 

2101class ScaleGuru(Guru): 

2102 

2103 ''' 

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

2105 

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

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

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

2109 arguments, which are required for most GMT commands. 

2110 

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

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

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

2114 

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

2116 

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

2118 limits imposed on other axes. 

2119 

2120 ''' 

2121 

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

2123 percent_interval=None, copy_from=None): 

2124 

2125 Guru.__init__(self) 

2126 

2127 if copy_from: 

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

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

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

2131 self.aspect = copy_from.aspect 

2132 

2133 if percent_interval is not None: 

2134 from scipy.stats import scoreatpercentile as scap 

2135 

2136 self.templates = dict( 

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

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

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

2140 

2141 maxdim = 2 

2142 if data_tuples: 

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

2144 else: 

2145 if axes: 

2146 maxdim = len(axes) 

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

2148 if axes is not None: 

2149 self.axes = axes 

2150 else: 

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

2152 

2153 # sophisticated data-range calculation 

2154 data_ranges = [None] * maxdim 

2155 for dt_ in data_tuples: 

2156 dt = num.asarray(dt_) 

2157 in_range = True 

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

2159 if ax.limits and ax.masking: 

2160 ax_limits = list(ax.limits) 

2161 if ax_limits[0] is None: 

2162 ax_limits[0] = -num.inf 

2163 if ax_limits[1] is None: 

2164 ax_limits[1] = num.inf 

2165 in_range = num.logical_and( 

2166 in_range, 

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

2168 

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

2170 

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

2172 if len(x) >= 1: 

2173 if in_range is not True: 

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

2175 if percent_interval is None: 

2176 range_this = ( 

2177 num.nanmin(xmasked), 

2178 num.nanmax(xmasked)) 

2179 else: 

2180 xmasked_finite = num.compress( 

2181 num.isfinite(xmasked), xmasked) 

2182 range_this = ( 

2183 scap(xmasked_finite, 

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

2185 scap(xmasked_finite, 

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

2187 else: 

2188 if percent_interval is None: 

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

2190 else: 

2191 xmasked_finite = num.compress( 

2192 num.isfinite(xmasked), xmasked) 

2193 range_this = ( 

2194 scap(xmasked_finite, 

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

2196 scap(xmasked_finite, 

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

2198 else: 

2199 range_this = (0., 1.) 

2200 

2201 if ax.limits: 

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

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

2204 range_this[1]) 

2205 

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

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

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

2209 

2210 else: 

2211 range_this = ax.limits 

2212 

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

2214 data_ranges[i] = range_this 

2215 else: 

2216 mi, ma = range_this 

2217 if data_ranges[i] is not None: 

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

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

2220 

2221 data_ranges[i] = (mi, ma) 

2222 

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

2224 if data_ranges[i] is None or not ( 

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

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

2227 

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

2229 

2230 self.data_ranges = data_ranges 

2231 self.aspect = aspect 

2232 

2233 def copy(self): 

2234 return ScaleGuru(copy_from=self) 

2235 

2236 def get_params(self, ax_projection=False): 

2237 

2238 ''' 

2239 Get dict with output parameters. 

2240 

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

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

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

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

2245 

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

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

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

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

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

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

2252 label string. 

2253 ''' 

2254 

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

2256 self.data_ranges[0], ax_projection) 

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

2258 self.data_ranges[1], ax_projection) 

2259 if len(self.axes) > 2: 

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

2261 self.data_ranges[2], ax_projection) 

2262 

2263 # enforce certain aspect, if needed 

2264 if self.aspect is not None: 

2265 xwid = xma-xmi 

2266 ywid = yma-ymi 

2267 if ywid < xwid*self.aspect: 

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

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

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

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

2272 override_scaled_unit_factor=1.) 

2273 

2274 elif xwid < ywid/self.aspect: 

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

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

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

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

2279 override_scaled_unit_factor=1.) 

2280 

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

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

2283 if len(self.axes) > 2: 

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

2285 

2286 return params 

2287 

2288 

2289class GumSpring(object): 

2290 

2291 ''' 

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

2293 ''' 

2294 

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

2296 self.minimal = minimal 

2297 if grow is None: 

2298 if minimal is None: 

2299 self.grow = 1.0 

2300 else: 

2301 self.grow = 0.0 

2302 else: 

2303 self.grow = grow 

2304 self.value = 1.0 

2305 

2306 def get_minimal(self): 

2307 if self.minimal is not None: 

2308 return self.minimal 

2309 else: 

2310 return 0.0 

2311 

2312 def get_grow(self): 

2313 return self.grow 

2314 

2315 def set_value(self, value): 

2316 self.value = value 

2317 

2318 def get_value(self): 

2319 return self.value 

2320 

2321 

2322def distribute(sizes, grows, space): 

2323 sizes = list(sizes) 

2324 gsum = sum(grows) 

2325 if gsum > 0.0: 

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

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

2328 return sizes 

2329 

2330 

2331class Widget(Guru): 

2332 

2333 ''' 

2334 Base class of the gmtpy layout system. 

2335 

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

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

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

2339 

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

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

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

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

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

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

2346 

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

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

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

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

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

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

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

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

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

2356 

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

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

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

2360 ''' 

2361 

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

2363 

2364 ''' 

2365 Create new widget. 

2366 ''' 

2367 

2368 Guru.__init__(self) 

2369 

2370 self.templates = dict( 

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

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

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

2374 

2375 if horizontal is None: 

2376 self.horizontal = GumSpring() 

2377 else: 

2378 self.horizontal = horizontal 

2379 

2380 if vertical is None: 

2381 self.vertical = GumSpring() 

2382 else: 

2383 self.vertical = vertical 

2384 

2385 self.aspect = None 

2386 self.parent = parent 

2387 self.dirty = True 

2388 

2389 def set_parent(self, parent): 

2390 

2391 ''' 

2392 Set the parent widget. 

2393 

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

2395 methods are responsible for calling this. 

2396 ''' 

2397 

2398 self.parent = parent 

2399 self.dirtyfy() 

2400 

2401 def get_parent(self): 

2402 

2403 ''' 

2404 Get the widgets parent widget. 

2405 ''' 

2406 

2407 return self.parent 

2408 

2409 def get_root(self): 

2410 

2411 ''' 

2412 Get the root widget in the layout hierarchy. 

2413 ''' 

2414 

2415 if self.parent is not None: 

2416 return self.get_parent() 

2417 else: 

2418 return self 

2419 

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

2421 

2422 ''' 

2423 Set the horizontal sizing policy of the Widget. 

2424 

2425 

2426 :param minimal: new minimal width of the widget 

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

2428 ''' 

2429 

2430 self.horizontal = GumSpring(minimal, grow) 

2431 self.dirtyfy() 

2432 

2433 def get_horizontal(self): 

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

2435 

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

2437 

2438 ''' 

2439 Set the horizontal sizing policy of the Widget. 

2440 

2441 :param minimal: new minimal height of the widget 

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

2443 ''' 

2444 

2445 self.vertical = GumSpring(minimal, grow) 

2446 self.dirtyfy() 

2447 

2448 def get_vertical(self): 

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

2450 

2451 def set_aspect(self, aspect=None): 

2452 

2453 ''' 

2454 Set aspect constraint on the widget. 

2455 

2456 The aspect is given as height divided by width. 

2457 ''' 

2458 

2459 self.aspect = aspect 

2460 self.dirtyfy() 

2461 

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

2463 

2464 ''' 

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

2466 call. 

2467 ''' 

2468 

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

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

2471 self.set_aspect(aspect) 

2472 

2473 def get_policy(self): 

2474 mh, gh = self.get_horizontal() 

2475 mv, gv = self.get_vertical() 

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

2477 

2478 def legalize(self, size, offset): 

2479 

2480 ''' 

2481 Get legal size for widget. 

2482 

2483 Returns: (new_size, new_offset) 

2484 

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

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

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

2488 ''' 

2489 

2490 sh, sv = size 

2491 oh, ov = offset 

2492 shs, svs = Widget.get_min_size(self) 

2493 ghs, gvs = Widget.get_grow(self) 

2494 

2495 if ghs == 0.0: 

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

2497 sh = shs 

2498 

2499 if gvs == 0.0: 

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

2501 sv = svs 

2502 

2503 if self.aspect is not None: 

2504 if sh > sv/self.aspect: 

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

2506 sh = sv/self.aspect 

2507 if sv > sh*self.aspect: 

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

2509 sv = sh*self.aspect 

2510 

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

2512 

2513 def get_min_size(self): 

2514 

2515 ''' 

2516 Get minimum size of widget. 

2517 

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

2519 ''' 

2520 

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

2522 if self.aspect is not None: 

2523 if mv == 0.0: 

2524 return mh, mh*self.aspect 

2525 elif mh == 0.0: 

2526 return mv/self.aspect, mv 

2527 return mh, mv 

2528 

2529 def get_grow(self): 

2530 

2531 ''' 

2532 Get widget's desire to grow. 

2533 

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

2535 ''' 

2536 

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

2538 

2539 def set_size(self, size, offset): 

2540 

2541 ''' 

2542 Set the widget's current size. 

2543 

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

2545 responsibility to call this. 

2546 ''' 

2547 

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

2549 self.offset = inner_offset 

2550 self.horizontal.set_value(sh) 

2551 self.vertical.set_value(sv) 

2552 self.dirty = False 

2553 

2554 def __str__(self): 

2555 

2556 def indent(ind, str): 

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

2558 size, offset = self.get_size() 

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

2560 children = self.get_children() 

2561 if children: 

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

2563 return s 

2564 

2565 def policies_debug_str(self): 

2566 

2567 def indent(ind, str): 

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

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

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

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

2572 

2573 children = self.get_children() 

2574 if children: 

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

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

2577 return s 

2578 

2579 def get_corners(self, descend=False): 

2580 

2581 ''' 

2582 Get coordinates of the corners of the widget. 

2583 

2584 Returns list with coordinate tuples. 

2585 

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

2587 coordinates of all sub-widgets. 

2588 ''' 

2589 

2590 self.do_layout() 

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

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

2593 if descend: 

2594 for child in self.get_children(): 

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

2596 return corners 

2597 

2598 def get_sizes(self): 

2599 

2600 ''' 

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

2602 

2603 Returns a list with size tuples. 

2604 ''' 

2605 self.do_layout() 

2606 sizes = [self.get_size()] 

2607 for child in self.get_children(): 

2608 sizes.extend(child.get_sizes()) 

2609 return sizes 

2610 

2611 def do_layout(self): 

2612 

2613 ''' 

2614 Triggers layouting of the widget hierarchy, if needed. 

2615 ''' 

2616 

2617 if self.parent is not None: 

2618 return self.parent.do_layout() 

2619 

2620 if not self.dirty: 

2621 return 

2622 

2623 sh, sv = self.get_min_size() 

2624 gh, gv = self.get_grow() 

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

2626 sh = 15.*cm 

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

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

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

2630 

2631 def get_children(self): 

2632 

2633 ''' 

2634 Get sub-widgets contained in this widget. 

2635 

2636 Returns a list of widgets. 

2637 ''' 

2638 

2639 return [] 

2640 

2641 def get_size(self): 

2642 

2643 ''' 

2644 Get current size and position of the widget. 

2645 

2646 Triggers layouting and returns 

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

2648 ''' 

2649 

2650 self.do_layout() 

2651 return (self.horizontal.get_value(), 

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

2653 

2654 def get_params(self): 

2655 

2656 ''' 

2657 Get current size and position of the widget. 

2658 

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

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

2661 ''' 

2662 

2663 self.do_layout() 

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

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

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

2667 

2668 def width(self): 

2669 

2670 ''' 

2671 Get current width of the widget. 

2672 

2673 Triggers layouting and returns width. 

2674 ''' 

2675 

2676 self.do_layout() 

2677 return self.horizontal.get_value() 

2678 

2679 def height(self): 

2680 

2681 ''' 

2682 Get current height of the widget. 

2683 

2684 Triggers layouting and return height. 

2685 ''' 

2686 

2687 self.do_layout() 

2688 return self.vertical.get_value() 

2689 

2690 def bbox(self): 

2691 

2692 ''' 

2693 Get PostScript bounding box for this widget. 

2694 

2695 Triggers layouting and returns values suitable to create PS bounding 

2696 box, representing the widgets current size and position. 

2697 ''' 

2698 

2699 self.do_layout() 

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

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

2702 

2703 def dirtyfy(self): 

2704 

2705 ''' 

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

2707 

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

2709 new layouting. 

2710 ''' 

2711 

2712 if self.parent is not None: 

2713 self.parent.dirtyfy() 

2714 

2715 self.dirty = True 

2716 

2717 

2718class CenterLayout(Widget): 

2719 

2720 ''' 

2721 A layout manager which centers its single child widget. 

2722 

2723 The child widget may be oversized. 

2724 ''' 

2725 

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

2727 Widget.__init__(self, horizontal, vertical) 

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

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

2730 

2731 def get_min_size(self): 

2732 shs, svs = Widget.get_min_size(self) 

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

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

2735 

2736 def get_grow(self): 

2737 ghs, gvs = Widget.get_grow(self) 

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

2739 return gh*ghs, gv*gvs 

2740 

2741 def set_size(self, size, offset): 

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

2743 

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

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

2746 if ghc != 0.: 

2747 shc = sh 

2748 if gvc != 0.: 

2749 svc = sv 

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

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

2752 

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

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

2755 

2756 def set_widget(self, widget=None): 

2757 

2758 ''' 

2759 Set the child widget, which shall be centered. 

2760 ''' 

2761 

2762 if widget is None: 

2763 widget = Widget() 

2764 

2765 self.content = widget 

2766 

2767 widget.set_parent(self) 

2768 

2769 def get_widget(self): 

2770 return self.content 

2771 

2772 def get_children(self): 

2773 return [self.content] 

2774 

2775 

2776class FrameLayout(Widget): 

2777 

2778 ''' 

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

2780 widgets. 

2781 

2782 :: 

2783 

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

2785 | top | 

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

2787 | | | | 

2788 | left | center | right | 

2789 | | | | 

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

2791 | bottom | 

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

2793 

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

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

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

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

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

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

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

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

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

2803 spaces between the widgets. 

2804 ''' 

2805 

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

2807 Widget.__init__(self, horizontal, vertical) 

2808 mw = 3.*cm 

2809 self.left = Widget( 

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

2811 self.right = Widget( 

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

2813 self.top = Widget( 

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

2815 parent=self) 

2816 self.bottom = Widget( 

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

2818 parent=self) 

2819 self.center = Widget( 

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

2821 parent=self) 

2822 

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

2824 ''' 

2825 Give margins fixed size constraints. 

2826 ''' 

2827 

2828 self.left.set_horizontal(left, 0) 

2829 self.right.set_horizontal(right, 0) 

2830 self.top.set_vertical(top, 0) 

2831 self.bottom.set_vertical(bottom, 0) 

2832 

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

2834 ''' 

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

2836 

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

2838 ''' 

2839 self.left.set_horizontal(left, grow) 

2840 self.right.set_horizontal(right, grow) 

2841 self.top.set_vertical(top, grow) 

2842 self.bottom.set_vertical(bottom, grow) 

2843 

2844 def get_min_size(self): 

2845 shs, svs = Widget.get_min_size(self) 

2846 

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

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

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

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

2851 

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

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

2854 

2855 # prevent widgets from collapsing 

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

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

2858 shsum += 0.1*cm 

2859 

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

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

2862 svsum += 0.1*cm 

2863 

2864 sh = max(shs, shsum) 

2865 sv = max(svs, svsum) 

2866 

2867 return sh, sv 

2868 

2869 def get_grow(self): 

2870 ghs, gvs = Widget.get_grow(self) 

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

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

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

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

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

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

2877 return gh, gv 

2878 

2879 def set_size(self, size, offset): 

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

2881 

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

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

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

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

2886 

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

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

2889 

2890 if ah < 0.0: 

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

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

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

2894 if av < 0.0: 

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

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

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

2898 

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

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

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

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

2903 

2904 if self.center.aspect is not None: 

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

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

2907 if 0.0 < ahm < ah: 

2908 slh, srh, sch = distribute( 

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

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

2911 

2912 elif 0.0 < avm < av: 

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

2914 sch*self.center.aspect), 

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

2916 

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

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

2919 

2920 oh += ah/2. 

2921 ov += av/2. 

2922 sh -= ah 

2923 sv -= av 

2924 

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

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

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

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

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

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

2931 

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

2933 

2934 ''' 

2935 Set one of the sub-widgets. 

2936 

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

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

2939 ''' 

2940 

2941 if widget is None: 

2942 widget = Widget() 

2943 

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

2945 self.__dict__[which] = widget 

2946 else: 

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

2948 

2949 widget.set_parent(self) 

2950 

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

2952 

2953 ''' 

2954 Get one of the sub-widgets. 

2955 

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

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

2958 ''' 

2959 

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

2961 return self.__dict__[which] 

2962 else: 

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

2964 

2965 def get_children(self): 

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

2967 

2968 

2969class GridLayout(Widget): 

2970 

2971 ''' 

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

2973 

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

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

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

2977 

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

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

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

2981 might not be resolved optimally. 

2982 ''' 

2983 

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

2985 

2986 ''' 

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

2988 ''' 

2989 

2990 Widget.__init__(self, horizontal, vertical) 

2991 self.grid = [] 

2992 for iy in range(ny): 

2993 row = [] 

2994 for ix in range(nx): 

2995 w = Widget(parent=self) 

2996 row.append(w) 

2997 

2998 self.grid.append(row) 

2999 

3000 def sub_min_sizes_as_array(self): 

3001 esh = num.array( 

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

3003 dtype=float) 

3004 esv = num.array( 

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

3006 dtype=float) 

3007 return esh, esv 

3008 

3009 def sub_grows_as_array(self): 

3010 egh = num.array( 

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

3012 dtype=float) 

3013 egv = num.array( 

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

3015 dtype=float) 

3016 return egh, egv 

3017 

3018 def get_min_size(self): 

3019 sh, sv = Widget.get_min_size(self) 

3020 esh, esv = self.sub_min_sizes_as_array() 

3021 if esh.size != 0: 

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

3023 if esv.size != 0: 

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

3025 return sh, sv 

3026 

3027 def get_grow(self): 

3028 ghs, gvs = Widget.get_grow(self) 

3029 egh, egv = self.sub_grows_as_array() 

3030 if egh.size != 0: 

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

3032 else: 

3033 gh = 1.0 

3034 if egv.size != 0: 

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

3036 else: 

3037 gv = 1.0 

3038 return gh, gv 

3039 

3040 def set_size(self, size, offset): 

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

3042 esh, esv = self.sub_min_sizes_as_array() 

3043 egh, egv = self.sub_grows_as_array() 

3044 

3045 # available additional space 

3046 empty = esh.size == 0 

3047 

3048 if not empty: 

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

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

3051 else: 

3052 av = sv 

3053 ah = sh 

3054 

3055 if ah < 0.0: 

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

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

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

3059 if av < 0.0: 

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

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

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

3063 

3064 nx, ny = esh.shape 

3065 

3066 if not empty: 

3067 # distribute additional space on rows and columns 

3068 # according to grow weights and minimal sizes 

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

3070 nesh = esh.copy() 

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

3072 

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

3074 

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

3076 nesv = esv.copy() 

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

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

3079 

3080 ah = sh - sum(nsh) 

3081 av = sv - sum(nsv) 

3082 

3083 oh += ah/2. 

3084 ov += av/2. 

3085 sh -= ah 

3086 sv -= av 

3087 

3088 # resize child widgets 

3089 neov = ov + sum(nsv) 

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

3091 neov -= nesv 

3092 neoh = oh 

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

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

3095 neoh += nesh 

3096 

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

3098 

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

3100 

3101 ''' 

3102 Set one of the sub-widgets. 

3103 

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

3105 counted from zero. 

3106 ''' 

3107 

3108 if widget is None: 

3109 widget = Widget() 

3110 

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

3112 widget.set_parent(self) 

3113 

3114 def get_widget(self, ix, iy): 

3115 

3116 ''' 

3117 Get one of the sub-widgets. 

3118 

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

3120 counted from zero. 

3121 ''' 

3122 

3123 return self.grid[iy][ix] 

3124 

3125 def get_children(self): 

3126 children = [] 

3127 for row in self.grid: 

3128 children.extend(row) 

3129 

3130 return children 

3131 

3132 

3133def is_gmt5(version='newest'): 

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

3135 

3136 

3137def is_gmt6(version='newest'): 

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

3139 

3140 

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

3142 

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

3144 

3145 if gmt.is_gmt5(): 

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

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

3148 gmt.save(fn, crop_eps_mode=True) 

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

3150 s = f.read() 

3151 

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

3153 else: 

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

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

3156 

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

3158 

3159 

3160def text_box( 

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

3162 

3163 gmt = GMT(version=gmtversion) 

3164 if gmt.is_gmt5(): 

3165 row = [0, 0, text] 

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

3167 else: 

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

3169 farg = [] 

3170 

3171 gmt.pstext( 

3172 in_rows=[row], 

3173 finish=True, 

3174 R=(0, 1, 0, 1), 

3175 J='x10p', 

3176 N=True, 

3177 *farg, 

3178 **kwargs) 

3179 

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

3181 gmt.save(fn) 

3182 

3183 (_, stderr) = subprocess.Popen( 

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

3185 stderr=subprocess.PIPE).communicate() 

3186 

3187 dx, dy = None, None 

3188 for line in stderr.splitlines(): 

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

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

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

3192 break 

3193 

3194 return dx, dy 

3195 

3196 

3197class TableLiner(object): 

3198 ''' 

3199 Utility class to turn tables into lines. 

3200 ''' 

3201 

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

3203 self.in_columns = in_columns 

3204 self.in_rows = in_rows 

3205 self.encoding = encoding 

3206 

3207 def __iter__(self): 

3208 if self.in_columns is not None: 

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

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

3211 self.encoding) 

3212 

3213 if self.in_rows is not None: 

3214 for row in self.in_rows: 

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

3216 self.encoding) 

3217 

3218 

3219class LineStreamChopper(object): 

3220 ''' 

3221 File-like object to buffer data. 

3222 ''' 

3223 

3224 def __init__(self, liner): 

3225 self.chopsize = None 

3226 self.liner = liner 

3227 self.chop_iterator = None 

3228 self.closed = False 

3229 

3230 def _chopiter(self): 

3231 buf = BytesIO() 

3232 for line in self.liner: 

3233 buf.write(line) 

3234 buflen = buf.tell() 

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

3236 buf.seek(0) 

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

3238 yield buf.read(self.chopsize) 

3239 

3240 newbuf = BytesIO() 

3241 newbuf.write(buf.read()) 

3242 buf.close() 

3243 buf = newbuf 

3244 

3245 yield buf.getvalue() 

3246 buf.close() 

3247 

3248 def read(self, size=None): 

3249 if self.closed: 

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

3251 if self.chop_iterator is None: 

3252 self.chopsize = size 

3253 self.chop_iterator = self._chopiter() 

3254 

3255 self.chopsize = size 

3256 try: 

3257 return next(self.chop_iterator) 

3258 except StopIteration: 

3259 return '' 

3260 

3261 def close(self): 

3262 self.chopsize = None 

3263 self.chop_iterator = None 

3264 self.closed = True 

3265 

3266 def flush(self): 

3267 pass 

3268 

3269 

3270font_tab = { 

3271 0: 'Helvetica', 

3272 1: 'Helvetica-Bold', 

3273} 

3274 

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

3276 

3277 

3278class GMT(object): 

3279 ''' 

3280 A thin wrapper to GMT command execution. 

3281 

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

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

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

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

3286 gmtpy and gmtpy must know where to find it. 

3287 

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

3289 output file. 

3290 

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

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

3293 

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

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

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

3297 

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

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

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

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

3302 

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

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

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

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

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

3308 execution of more than one GMT instance. 

3309 

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

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

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

3313 backward compatibility of the scripts can be maintained. 

3314 

3315 ''' 

3316 

3317 def __init__( 

3318 self, 

3319 config=None, 

3320 kontinue=None, 

3321 version='newest', 

3322 config_papersize=None, 

3323 eps_mode=False): 

3324 

3325 self.installation = get_gmt_installation(version) 

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

3327 self.eps_mode = eps_mode 

3328 self._shutil = shutil 

3329 

3330 if config: 

3331 self.gmt_config.update(config) 

3332 

3333 if config_papersize: 

3334 if not isinstance(config_papersize, str): 

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

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

3337 

3338 if self.is_gmt5(): 

3339 self.gmt_config['PS_MEDIA'] = config_papersize 

3340 else: 

3341 self.gmt_config['PAPER_MEDIA'] = config_papersize 

3342 

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

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

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

3346 

3347 if kontinue is not None: 

3348 self.load_unfinished(kontinue) 

3349 self.needstart = False 

3350 else: 

3351 self.output = BytesIO() 

3352 self.needstart = True 

3353 

3354 self.finished = False 

3355 

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

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

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

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

3360 

3361 self.layout = None 

3362 self.command_log = [] 

3363 self.keep_temp_dir = False 

3364 

3365 def is_gmt5(self): 

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

3367 

3368 def is_gmt6(self): 

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

3370 

3371 def get_version(self): 

3372 return self.installation['version'] 

3373 

3374 def get_config(self, key): 

3375 return self.gmt_config[key] 

3376 

3377 def to_points(self, string): 

3378 if not string: 

3379 return 0 

3380 

3381 unit = string[-1] 

3382 if unit in _units: 

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

3384 else: 

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

3386 return float(string)/_units[default_unit] 

3387 

3388 def label_font_size(self): 

3389 if self.is_gmt5(): 

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

3391 else: 

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

3393 

3394 def label_font(self): 

3395 if self.is_gmt5(): 

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

3397 else: 

3398 return self.gmt_config['LABEL_FONT'] 

3399 

3400 def gen_gmt_config_file(self, config_filename, config): 

3401 f = open(config_filename, 'wb') 

3402 f.write( 

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

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

3405 

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

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

3408 f.close() 

3409 

3410 def __del__(self): 

3411 if not self.keep_temp_dir: 

3412 self._shutil.rmtree(self.tempdir) 

3413 

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

3415 

3416 ''' 

3417 Execute arbitrary GMT command. 

3418 

3419 See docstring in __getattr__ for details. 

3420 ''' 

3421 

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

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

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

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

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

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

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

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

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

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

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

3433 

3434 assert not self.finished 

3435 

3436 # check for mutual exclusiveness on input and output possibilities 

3437 assert (1 >= len( 

3438 [x for x in [ 

3439 in_stream, in_filename, in_string, in_columns, in_rows] 

3440 if x is not None])) 

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

3442 if x is not None])) 

3443 

3444 options = [] 

3445 

3446 gmt_config = self.gmt_config 

3447 if not self.is_gmt5(): 

3448 gmt_config_filename = self.gmt_config_filename 

3449 if config_override: 

3450 gmt_config = self.gmt_config.copy() 

3451 gmt_config.update(config_override) 

3452 gmt_config_override_filename = pjoin( 

3453 self.tempdir, 'gmtdefaults_override') 

3454 self.gen_gmt_config_file( 

3455 gmt_config_override_filename, gmt_config) 

3456 gmt_config_filename = gmt_config_override_filename 

3457 

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

3459 if config_override: 

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

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

3462 

3463 if out_discard: 

3464 out_filename = '/dev/null' 

3465 

3466 out_mustclose = False 

3467 if out_filename is not None: 

3468 out_mustclose = True 

3469 out_stream = open(out_filename, 'wb') 

3470 

3471 if in_filename is not None: 

3472 in_stream = open(in_filename, 'rb') 

3473 

3474 if in_string is not None: 

3475 in_stream = BytesIO(in_string) 

3476 

3477 encoding_gmt = gmt_config.get( 

3478 'PS_CHAR_ENCODING', 

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

3480 

3481 encoding = encoding_gmt_to_python[encoding_gmt.lower()] 

3482 

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

3484 in_stream = LineStreamChopper(TableLiner(in_columns=in_columns, 

3485 in_rows=in_rows, 

3486 encoding=encoding)) 

3487 

3488 # convert option arguments to strings 

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

3490 if len(k) > 1: 

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

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

3493 % (k, command)) 

3494 

3495 if type(v) is bool: 

3496 if v: 

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

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

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

3500 else: 

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

3502 

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

3504 if out_stream is None: 

3505 if not finish: 

3506 options.append('-K') 

3507 else: 

3508 self.finished = True 

3509 

3510 if not self.needstart: 

3511 options.append('-O') 

3512 else: 

3513 self.needstart = False 

3514 

3515 out_stream = self.output 

3516 

3517 # run the command 

3518 if self.is_gmt5(): 

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

3520 else: 

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

3522 

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

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

3525 args.extend(options) 

3526 args.extend(addargs) 

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

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

3529 args.append('+'+gmt_config_filename) 

3530 

3531 bs = 2048 

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

3533 stdout=subprocess.PIPE, bufsize=bs, 

3534 env=self.environ) 

3535 while True: 

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

3537 if cr: 

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

3539 if cw: 

3540 if in_stream is not None: 

3541 data = in_stream.read(bs) 

3542 if len(data) == 0: 

3543 break 

3544 p.stdin.write(data) 

3545 else: 

3546 break 

3547 if not cr and not cw: 

3548 break 

3549 

3550 p.stdin.close() 

3551 

3552 while True: 

3553 data = p.stdout.read(bs) 

3554 if len(data) == 0: 

3555 break 

3556 out_stream.write(data) 

3557 

3558 p.stdout.close() 

3559 

3560 retcode = p.wait() 

3561 

3562 if in_stream is not None: 

3563 in_stream.close() 

3564 

3565 if out_mustclose: 

3566 out_stream.close() 

3567 

3568 if retcode != 0: 

3569 self.keep_temp_dir = True 

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

3571 'While executing command:\n%s' 

3572 % (command, escape_shell_args(args))) 

3573 

3574 self.command_log.append(args) 

3575 

3576 def __getattr__(self, command): 

3577 

3578 ''' 

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

3580 

3581 Execute arbitrary GMT command. 

3582 

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

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

3585 called. 

3586 

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

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

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

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

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

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

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

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

3595 

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

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

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

3599 not interested in the output. 

3600 

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

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

3603 

3604 =============== ======================================================= 

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

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

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

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

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

3610 ascii 

3611 table, which is fed to the process. 

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

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

3614 table, which is fed to the process. 

3615 =============== ======================================================= 

3616 

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

3618 following options: 

3619 

3620 ================= ===================================================== 

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

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

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

3624 ================= ===================================================== 

3625 

3626 Additional keyword arguments: 

3627 

3628 ===================== ================================================= 

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

3630 currently active set of defaults exclusively 

3631 during this call. 

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

3633 by the GMT instance is finished, and no further 

3634 plotting is allowed. 

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

3636 option to the command. 

3637 ===================== ================================================= 

3638 

3639 ''' 

3640 

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

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

3643 return f 

3644 

3645 def tempfilename(self, name=None): 

3646 ''' 

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

3648 

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

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

3651 ''' 

3652 

3653 if not name: 

3654 name = ''.join( 

3655 [random.choice('abcdefghijklmnopqrstuvwxyz') 

3656 for i in range(10)]) 

3657 

3658 fn = pjoin(self.tempdir, name) 

3659 return fn 

3660 

3661 def tempfile(self, name=None): 

3662 ''' 

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

3664 ''' 

3665 

3666 fn = self.tempfilename(name) 

3667 f = open(fn, 'wb') 

3668 return f, fn 

3669 

3670 def save_unfinished(self, filename): 

3671 out = open(filename, 'wb') 

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

3673 out.close() 

3674 

3675 def load_unfinished(self, filename): 

3676 self.output = BytesIO() 

3677 self.finished = False 

3678 inp = open(filename, 'rb') 

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

3680 inp.close() 

3681 

3682 def dump(self, ident): 

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

3684 self.save_unfinished(filename) 

3685 

3686 def load(self, ident): 

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

3688 self.load_unfinished(filename) 

3689 

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

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

3692 psconvert=False): 

3693 

3694 ''' 

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

3696 

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

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

3699 

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

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

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

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

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

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

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

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

3708 

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

3710 ''' 

3711 

3712 if not self.finished: 

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

3714 

3715 if filename: 

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

3717 out = open(tempfn, 'wb') 

3718 else: 

3719 out = sys.stdout 

3720 

3721 if bbox and not self.is_gmt5(): 

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

3723 else: 

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

3725 

3726 if filename: 

3727 out.close() 

3728 

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

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

3731 

3732 shutil.move(tempfn, filename) 

3733 return 

3734 

3735 if self.is_gmt5(): 

3736 if crop_eps_mode: 

3737 addarg = ['-A'] 

3738 else: 

3739 addarg = [] 

3740 

3741 subprocess.call( 

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

3743 '-Te', '-F%s' % tempfn, tempfn, ] + addarg) 

3744 

3745 if bbox: 

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

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

3748 replace_bbox(bbox, fin, fout) 

3749 

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

3751 

3752 else: 

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

3754 

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

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

3757 return 

3758 

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

3760 if psconvert: 

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

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

3763 '-F' + filename]) 

3764 else: 

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

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

3767 else: 

3768 subprocess.call([ 

3769 'gmtpy-epstopdf', 

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

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

3772 

3773 convert_graph( 

3774 tempfn + '.pdf', filename, 

3775 resolution=resolution, oversample=oversample, 

3776 size=size, width=width, height=height) 

3777 

3778 def bbox(self): 

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

3780 

3781 def get_command_log(self): 

3782 ''' 

3783 Get the command log. 

3784 ''' 

3785 

3786 return self.command_log 

3787 

3788 def __str__(self): 

3789 s = '' 

3790 for com in self.command_log: 

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

3792 return s 

3793 

3794 def page_size_points(self): 

3795 ''' 

3796 Try to get paper size of output postscript file in points. 

3797 ''' 

3798 

3799 pm = paper_media(self.gmt_config).lower() 

3800 if pm.endswith('+') or pm.endswith('-'): 

3801 pm = pm[:-1] 

3802 

3803 orient = page_orientation(self.gmt_config).lower() 

3804 

3805 if pm in all_paper_sizes(): 

3806 

3807 if orient == 'portrait': 

3808 return get_paper_size(pm) 

3809 else: 

3810 return get_paper_size(pm)[1], get_paper_size(pm)[0] 

3811 

3812 m = re.match(r'custom_([0-9.]+)([cimp]?)x([0-9.]+)([cimp]?)', pm) 

3813 if m: 

3814 w, uw, h, uh = m.groups() 

3815 w, h = float(w), float(h) 

3816 if uw: 

3817 w *= _units[uw] 

3818 if uh: 

3819 h *= _units[uh] 

3820 if orient == 'portrait': 

3821 return w, h 

3822 else: 

3823 return h, w 

3824 

3825 return None, None 

3826 

3827 def default_layout(self, with_palette=False): 

3828 ''' 

3829 Get a default layout for the output page. 

3830 

3831 One of three different layouts is choosen, depending on the 

3832 `PAPER_MEDIA` setting in the GMT configuration dict. 

3833 

3834 If `PAPER_MEDIA` ends with a ``'+'`` (EPS output is selected), a 

3835 :py:class:`FrameLayout` is centered on the page, whose size is 

3836 controlled by its center widget's size plus the margins of the 

3837 :py:class:`FrameLayout`. 

3838 

3839 If `PAPER_MEDIA` indicates, that a custom page size is wanted by 

3840 starting with ``'Custom_'``, a :py:class:`FrameLayout` is used to fill 

3841 the complete page. The center widget's size is then controlled by the 

3842 page's size minus the margins of the :py:class:`FrameLayout`. 

3843 

3844 In any other case, two FrameLayouts are nested, such that the outer 

3845 layout attaches a 1 cm (printer) margin around the complete page, and 

3846 the inner FrameLayout's center widget takes up as much space as 

3847 possible under the constraint, that an aspect ratio of 1/golden_ratio 

3848 is preserved. 

3849 

3850 In any case, a reference to the innermost :py:class:`FrameLayout` 

3851 instance is returned. The top-level layout can be accessed by calling 

3852 :py:meth:`Widget.get_parent` on the returned layout. 

3853 ''' 

3854 

3855 if self.layout is None: 

3856 w, h = self.page_size_points() 

3857 

3858 if w is None or h is None: 

3859 raise GmtPyError("Can't determine page size for layout") 

3860 

3861 pm = paper_media(self.gmt_config).lower() 

3862 

3863 if with_palette: 

3864 palette_layout = GridLayout(3, 1) 

3865 spacer = palette_layout.get_widget(1, 0) 

3866 palette_widget = palette_layout.get_widget(2, 0) 

3867 spacer.set_horizontal(0.5*cm) 

3868 palette_widget.set_horizontal(0.5*cm) 

3869 

3870 if pm.endswith('+') or self.eps_mode: 

3871 outer = CenterLayout() 

3872 outer.set_policy((w, h), (0., 0.)) 

3873 inner = FrameLayout() 

3874 outer.set_widget(inner) 

3875 if with_palette: 

3876 inner.set_widget('center', palette_layout) 

3877 widget = palette_layout 

3878 else: 

3879 widget = inner.get_widget('center') 

3880 widget.set_policy((w/golden_ratio, 0.), (0., 0.), 

3881 aspect=1./golden_ratio) 

3882 mw = 3.0*cm 

3883 inner.set_fixed_margins( 

3884 mw, mw, mw/golden_ratio, mw/golden_ratio) 

3885 self.layout = inner 

3886 

3887 elif pm.startswith('custom_'): 

3888 layout = FrameLayout() 

3889 layout.set_policy((w, h), (0., 0.)) 

3890 mw = 3.0*cm 

3891 layout.set_min_margins( 

3892 mw, mw, mw/golden_ratio, mw/golden_ratio) 

3893 if with_palette: 

3894 layout.set_widget('center', palette_layout) 

3895 self.layout = layout 

3896 else: 

3897 outer = FrameLayout() 

3898 outer.set_policy((w, h), (0., 0.)) 

3899 outer.set_fixed_margins(1.*cm, 1.*cm, 1.*cm, 1.*cm) 

3900 

3901 inner = FrameLayout() 

3902 outer.set_widget('center', inner) 

3903 mw = 3.0*cm 

3904 inner.set_min_margins(mw, mw, mw/golden_ratio, mw/golden_ratio) 

3905 if with_palette: 

3906 inner.set_widget('center', palette_layout) 

3907 widget = palette_layout 

3908 else: 

3909 widget = inner.get_widget('center') 

3910 

3911 widget.set_aspect(1./golden_ratio) 

3912 

3913 self.layout = inner 

3914 

3915 return self.layout 

3916 

3917 def draw_layout(self, layout): 

3918 ''' 

3919 Use psxy to draw layout; for debugging 

3920 ''' 

3921 

3922 # corners = layout.get_corners(descend=True) 

3923 rects = num.array(layout.get_sizes(), dtype=float) 

3924 rects_wid = rects[:, 0, 0] 

3925 rects_hei = rects[:, 0, 1] 

3926 rects_center_x = rects[:, 1, 0] + rects_wid*0.5 

3927 rects_center_y = rects[:, 1, 1] + rects_hei*0.5 

3928 nrects = len(rects) 

3929 prects = (rects_center_x, rects_center_y, num.arange(nrects), 

3930 num.zeros(nrects), rects_hei, rects_wid) 

3931 

3932 # points = num.array(corners, dtype=float) 

3933 

3934 cptfile = self.tempfilename() + '.cpt' 

3935 self.makecpt( 

3936 C='ocean', 

3937 T='%g/%g/%g' % (-nrects, nrects, 1), 

3938 Z=True, 

3939 out_filename=cptfile, suppress_defaults=True) 

3940 

3941 bb = layout.bbox() 

3942 self.psxy( 

3943 in_columns=prects, 

3944 C=cptfile, 

3945 W='1p', 

3946 S='J', 

3947 R=(bb[0], bb[2], bb[1], bb[3]), 

3948 *layout.XYJ()) 

3949 

3950 

3951def simpleconf_to_ax(conf, axname): 

3952 c = {} 

3953 x = axname 

3954 for x in ('', axname): 

3955 for k in ('label', 'unit', 'scaled_unit', 'scaled_unit_factor', 

3956 'space', 'mode', 'approx_ticks', 'limits', 'masking', 'inc', 

3957 'snap'): 

3958 

3959 if x+k in conf: 

3960 c[k] = conf[x+k] 

3961 

3962 return Ax(**c) 

3963 

3964 

3965class DensityPlotDef(object): 

3966 def __init__(self, data, cpt='ocean', tension=0.7, size=(640, 480), 

3967 contour=False, method='surface', zscaler=None, **extra): 

3968 self.data = data 

3969 self.cpt = cpt 

3970 self.tension = tension 

3971 self.size = size 

3972 self.contour = contour 

3973 self.method = method 

3974 self.zscaler = zscaler 

3975 self.extra = extra 

3976 

3977 

3978class TextDef(object): 

3979 def __init__( 

3980 self, 

3981 data, 

3982 size=9, 

3983 justify='MC', 

3984 fontno=0, 

3985 offset=(0, 0), 

3986 color='black'): 

3987 

3988 self.data = data 

3989 self.size = size 

3990 self.justify = justify 

3991 self.fontno = fontno 

3992 self.offset = offset 

3993 self.color = color 

3994 

3995 

3996class Simple(object): 

3997 def __init__(self, gmtconfig=None, gmtversion='newest', **simple_config): 

3998 self.data = [] 

3999 self.symbols = [] 

4000 self.config = copy.deepcopy(simple_config) 

4001 self.gmtconfig = gmtconfig 

4002 self.density_plot_defs = [] 

4003 self.text_defs = [] 

4004 

4005 self.gmtversion = gmtversion 

4006 

4007 self.data_x = [] 

4008 self.symbols_x = [] 

4009 

4010 self.data_y = [] 

4011 self.symbols_y = [] 

4012 

4013 self.default_config = {} 

4014 self.set_defaults(width=15.*cm, 

4015 height=15.*cm / golden_ratio, 

4016 margins=(2.*cm, 2.*cm, 2.*cm, 2.*cm), 

4017 with_palette=False, 

4018 palette_offset=0.5*cm, 

4019 palette_width=None, 

4020 palette_height=None, 

4021 zlabeloffset=2*cm, 

4022 draw_layout=False) 

4023 

4024 self.setup_defaults() 

4025 self.fixate_widget_aspect = False 

4026 

4027 def setup_defaults(self): 

4028 pass 

4029 

4030 def set_defaults(self, **kwargs): 

4031 self.default_config.update(kwargs) 

4032 

4033 def plot(self, data, symbol=''): 

4034 self.data.append(data) 

4035 self.symbols.append(symbol) 

4036 

4037 def density_plot(self, data, **kwargs): 

4038 dpd = DensityPlotDef(data, **kwargs) 

4039 self.density_plot_defs.append(dpd) 

4040 

4041 def text(self, data, **kwargs): 

4042 dpd = TextDef(data, **kwargs) 

4043 self.text_defs.append(dpd) 

4044 

4045 def plot_x(self, data, symbol=''): 

4046 self.data_x.append(data) 

4047 self.symbols_x.append(symbol) 

4048 

4049 def plot_y(self, data, symbol=''): 

4050 self.data_y.append(data) 

4051 self.symbols_y.append(symbol) 

4052 

4053 def set(self, **kwargs): 

4054 self.config.update(kwargs) 

4055 

4056 def setup_base(self, conf): 

4057 w = conf.pop('width') 

4058 h = conf.pop('height') 

4059 margins = conf.pop('margins') 

4060 

4061 gmtconfig = {} 

4062 if self.gmtconfig is not None: 

4063 gmtconfig.update(self.gmtconfig) 

4064 

4065 gmt = GMT( 

4066 version=self.gmtversion, 

4067 config=gmtconfig, 

4068 config_papersize='Custom_%ix%i' % (w, h)) 

4069 

4070 layout = gmt.default_layout(with_palette=conf['with_palette']) 

4071 layout.set_min_margins(*margins) 

4072 if conf['with_palette']: 

4073 widget = layout.get_widget().get_widget(0, 0) 

4074 spacer = layout.get_widget().get_widget(1, 0) 

4075 spacer.set_horizontal(conf['palette_offset']) 

4076 palette_widget = layout.get_widget().get_widget(2, 0) 

4077 if conf['palette_width'] is not None: 

4078 palette_widget.set_horizontal(conf['palette_width']) 

4079 if conf['palette_height'] is not None: 

4080 palette_widget.set_vertical(conf['palette_height']) 

4081 widget.set_vertical(h-margins[2]-margins[3]-0.03*cm) 

4082 return gmt, layout, widget, palette_widget 

4083 else: 

4084 widget = layout.get_widget() 

4085 return gmt, layout, widget, None 

4086 

4087 def setup_projection(self, widget, scaler, conf): 

4088 pass 

4089 

4090 def setup_scaling(self, conf): 

4091 ndims = 2 

4092 if self.density_plot_defs: 

4093 ndims = 3 

4094 

4095 axes = [simpleconf_to_ax(conf, x) for x in 'xyz'[:ndims]] 

4096 

4097 data_all = [] 

4098 data_all.extend(self.data) 

4099 for dsd in self.density_plot_defs: 

4100 if dsd.zscaler is None: 

4101 data_all.append(dsd.data) 

4102 else: 

4103 data_all.append(dsd.data[:2]) 

4104 data_chopped = [ds[:ndims] for ds in data_all] 

4105 

4106 scaler = ScaleGuru(data_chopped, axes=axes[:ndims]) 

4107 

4108 self.setup_scaling_plus(scaler, axes[:ndims]) 

4109 

4110 return scaler 

4111 

4112 def setup_scaling_plus(self, scaler, axes): 

4113 pass 

4114 

4115 def setup_scaling_extra(self, scaler, conf): 

4116 

4117 scaler_x = scaler.copy() 

4118 scaler_x.data_ranges[1] = (0., 1.) 

4119 scaler_x.axes[1].mode = 'off' 

4120 

4121 scaler_y = scaler.copy() 

4122 scaler_y.data_ranges[0] = (0., 1.) 

4123 scaler_y.axes[0].mode = 'off' 

4124 

4125 return scaler_x, scaler_y 

4126 

4127 def draw_density(self, gmt, widget, scaler): 

4128 

4129 R = scaler.R() 

4130 # par = scaler.get_params() 

4131 rxyj = R + widget.XYJ() 

4132 innerticks = False 

4133 for dpd in self.density_plot_defs: 

4134 

4135 fn_cpt = gmt.tempfilename() + '.cpt' 

4136 

4137 if dpd.zscaler is not None: 

4138 s = dpd.zscaler 

4139 else: 

4140 s = scaler 

4141 

4142 gmt.makecpt(C=dpd.cpt, out_filename=fn_cpt, *s.T()) 

4143 

4144 fn_grid = gmt.tempfilename() 

4145 

4146 fn_mean = gmt.tempfilename() 

4147 

4148 if dpd.method in ('surface', 'triangulate'): 

4149 gmt.blockmean(in_columns=dpd.data, 

4150 I='%i+/%i+' % dpd.size, # noqa 

4151 out_filename=fn_mean, *R) 

4152 

4153 if dpd.method == 'surface': 

4154 gmt.surface( 

4155 in_filename=fn_mean, 

4156 T=dpd.tension, 

4157 G=fn_grid, 

4158 I='%i+/%i+' % dpd.size, # noqa 

4159 out_discard=True, 

4160 *R) 

4161 

4162 if dpd.method == 'triangulate': 

4163 gmt.triangulate( 

4164 in_filename=fn_mean, 

4165 G=fn_grid, 

4166 I='%i+/%i+' % dpd.size, # noqa 

4167 out_discard=True, 

4168 V=True, 

4169 *R) 

4170 

4171 if gmt.is_gmt5(): 

4172 gmt.grdimage(fn_grid, C=fn_cpt, E='i', n='l', *rxyj) 

4173 

4174 else: 

4175 gmt.grdimage(fn_grid, C=fn_cpt, E='i', S='l', *rxyj) 

4176 

4177 if dpd.contour: 

4178 gmt.grdcontour(fn_grid, C=fn_cpt, W='0.5p,black', *rxyj) 

4179 innerticks = '0.5p,black' 

4180 

4181 os.remove(fn_grid) 

4182 os.remove(fn_mean) 

4183 

4184 if dpd.method == 'fillcontour': 

4185 extra = dict(C=fn_cpt) 

4186 extra.update(dpd.extra) 

4187 gmt.pscontour(in_columns=dpd.data, 

4188 I=True, *rxyj, **extra) # noqa 

4189 

4190 if dpd.method == 'contour': 

4191 extra = dict(W='0.5p,black', C=fn_cpt) 

4192 extra.update(dpd.extra) 

4193 gmt.pscontour(in_columns=dpd.data, *rxyj, **extra) 

4194 

4195 return fn_cpt, innerticks 

4196 

4197 def draw_basemap(self, gmt, widget, scaler): 

4198 gmt.psbasemap(*(widget.JXY() + scaler.RB(ax_projection=True))) 

4199 

4200 def draw(self, gmt, widget, scaler): 

4201 rxyj = scaler.R() + widget.JXY() 

4202 for dat, sym in zip(self.data, self.symbols): 

4203 gmt.psxy(in_columns=dat, *(sym.split()+rxyj)) 

4204 

4205 def post_draw(self, gmt, widget, scaler): 

4206 pass 

4207 

4208 def pre_draw(self, gmt, widget, scaler): 

4209 pass 

4210 

4211 def draw_extra(self, gmt, widget, scaler_x, scaler_y): 

4212 

4213 for dat, sym in zip(self.data_x, self.symbols_x): 

4214 gmt.psxy(in_columns=dat, 

4215 *(sym.split() + scaler_x.R() + widget.JXY())) 

4216 

4217 for dat, sym in zip(self.data_y, self.symbols_y): 

4218 gmt.psxy(in_columns=dat, 

4219 *(sym.split() + scaler_y.R() + widget.JXY())) 

4220 

4221 def draw_text(self, gmt, widget, scaler): 

4222 

4223 rxyj = scaler.R() + widget.JXY() 

4224 for td in self.text_defs: 

4225 x, y = td.data[0:2] 

4226 text = td.data[-1] 

4227 size = td.size 

4228 angle = 0 

4229 fontno = td.fontno 

4230 justify = td.justify 

4231 color = td.color 

4232 if gmt.is_gmt5(): 

4233 gmt.pstext( 

4234 in_rows=[(x, y, text)], 

4235 F='+f%gp,%s,%s+a%g+j%s' % ( 

4236 size, fontno, color, angle, justify), 

4237 D='%gp/%gp' % td.offset, *rxyj) 

4238 else: 

4239 gmt.pstext( 

4240 in_rows=[(x, y, size, angle, fontno, justify, text)], 

4241 D='%gp/%gp' % td.offset, *rxyj) 

4242 

4243 def save(self, filename, resolution=150): 

4244 

4245 conf = dict(self.default_config) 

4246 conf.update(self.config) 

4247 

4248 gmt, layout, widget, palette_widget = self.setup_base(conf) 

4249 scaler = self.setup_scaling(conf) 

4250 scaler_x, scaler_y = self.setup_scaling_extra(scaler, conf) 

4251 

4252 self.setup_projection(widget, scaler, conf) 

4253 if self.fixate_widget_aspect: 

4254 aspect = aspect_for_projection( 

4255 gmt.installation['version'], *(widget.J() + scaler.R())) 

4256 

4257 widget.set_aspect(aspect) 

4258 

4259 if conf['draw_layout']: 

4260 gmt.draw_layout(layout) 

4261 cptfile = None 

4262 if self.density_plot_defs: 

4263 cptfile, innerticks = self.draw_density(gmt, widget, scaler) 

4264 self.pre_draw(gmt, widget, scaler) 

4265 self.draw(gmt, widget, scaler) 

4266 self.post_draw(gmt, widget, scaler) 

4267 self.draw_extra(gmt, widget, scaler_x, scaler_y) 

4268 self.draw_text(gmt, widget, scaler) 

4269 self.draw_basemap(gmt, widget, scaler) 

4270 

4271 if palette_widget and cptfile: 

4272 nice_palette(gmt, palette_widget, scaler, cptfile, 

4273 innerticks=innerticks, 

4274 zlabeloffset=conf['zlabeloffset']) 

4275 

4276 gmt.save(filename, resolution=resolution) 

4277 

4278 

4279class LinLinPlot(Simple): 

4280 pass 

4281 

4282 

4283class LogLinPlot(Simple): 

4284 

4285 def setup_defaults(self): 

4286 self.set_defaults(xmode='min-max') 

4287 

4288 def setup_projection(self, widget, scaler, conf): 

4289 widget['J'] = '-JX%(width)gpl/%(height)gp' 

4290 scaler['B'] = '-B2:%(xlabel)s:/%(yinc)g:%(ylabel)s:WSen' 

4291 

4292 

4293class LinLogPlot(Simple): 

4294 

4295 def setup_defaults(self): 

4296 self.set_defaults(ymode='min-max') 

4297 

4298 def setup_projection(self, widget, scaler, conf): 

4299 widget['J'] = '-JX%(width)gp/%(height)gpl' 

4300 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/2:%(ylabel)s:WSen' 

4301 

4302 

4303class LogLogPlot(Simple): 

4304 

4305 def setup_defaults(self): 

4306 self.set_defaults(mode='min-max') 

4307 

4308 def setup_projection(self, widget, scaler, conf): 

4309 widget['J'] = '-JX%(width)gpl/%(height)gpl' 

4310 scaler['B'] = '-B2:%(xlabel)s:/2:%(ylabel)s:WSen' 

4311 

4312 

4313class AziDistPlot(Simple): 

4314 

4315 def __init__(self, *args, **kwargs): 

4316 Simple.__init__(self, *args, **kwargs) 

4317 self.fixate_widget_aspect = True 

4318 

4319 def setup_defaults(self): 

4320 self.set_defaults( 

4321 height=15.*cm, 

4322 width=15.*cm, 

4323 xmode='off', 

4324 xlimits=(0., 360.), 

4325 xinc=45.) 

4326 

4327 def setup_projection(self, widget, scaler, conf): 

4328 widget['J'] = '-JPa%(width)gp' 

4329 

4330 def setup_scaling_plus(self, scaler, axes): 

4331 scaler['B'] = '-B%(xinc)g:%(xlabel)s:/%(yinc)g:%(ylabel)s:N' 

4332 

4333 

4334class MPlot(Simple): 

4335 

4336 def __init__(self, *args, **kwargs): 

4337 Simple.__init__(self, *args, **kwargs) 

4338 self.fixate_widget_aspect = True 

4339 

4340 def setup_defaults(self): 

4341 self.set_defaults(xmode='min-max', ymode='min-max') 

4342 

4343 def setup_projection(self, widget, scaler, conf): 

4344 par = scaler.get_params() 

4345 lon0 = (par['xmin'] + par['xmax'])/2. 

4346 lat0 = (par['ymin'] + par['ymax'])/2. 

4347 sll = '%g/%g' % (lon0, lat0) 

4348 widget['J'] = '-JM' + sll + '/%(width)gp' 

4349 scaler['B'] = \ 

4350 '-B%(xinc)gg%(xinc)g:%(xlabel)s:/%(yinc)gg%(yinc)g:%(ylabel)s:WSen' 

4351 

4352 

4353def nice_palette(gmt, widget, scaleguru, cptfile, zlabeloffset=0.8*inch, 

4354 innerticks=True): 

4355 

4356 par = scaleguru.get_params() 

4357 par_ax = scaleguru.get_params(ax_projection=True) 

4358 nz_palette = int(widget.height()/inch * 300) 

4359 px = num.zeros(nz_palette*2) 

4360 px[1::2] += 1 

4361 pz = num.linspace(par['zmin'], par['zmax'], nz_palette).repeat(2) 

4362 pdz = pz[2]-pz[0] 

4363 palgrdfile = gmt.tempfilename() 

4364 pal_r = (0, 1, par['zmin'], par['zmax']) 

4365 pal_ax_r = (0, 1, par_ax['zmin'], par_ax['zmax']) 

4366 gmt.xyz2grd( 

4367 G=palgrdfile, R=pal_r, 

4368 I=(1, pdz), in_columns=(px, pz, pz), # noqa 

4369 out_discard=True) 

4370 

4371 gmt.grdimage(palgrdfile, R=pal_r, C=cptfile, *widget.JXY()) 

4372 if isinstance(innerticks, str): 

4373 tickpen = innerticks 

4374 gmt.grdcontour(palgrdfile, W=tickpen, R=pal_r, C=cptfile, 

4375 *widget.JXY()) 

4376 

4377 negpalwid = '%gp' % -widget.width() 

4378 if not isinstance(innerticks, str) and innerticks: 

4379 ticklen = negpalwid 

4380 else: 

4381 ticklen = '0p' 

4382 

4383 TICK_LENGTH_PARAM = 'MAP_TICK_LENGTH' if gmt.is_gmt5() else 'TICK_LENGTH' 

4384 gmt.psbasemap( 

4385 R=pal_ax_r, B='4::/%(zinc)g::nsw' % par_ax, 

4386 config={TICK_LENGTH_PARAM: ticklen}, 

4387 *widget.JXY()) 

4388 

4389 if innerticks: 

4390 gmt.psbasemap( 

4391 R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax, 

4392 config={TICK_LENGTH_PARAM: '0p'}, 

4393 *widget.JXY()) 

4394 else: 

4395 gmt.psbasemap(R=pal_ax_r, B='4::/%(zinc)g::E' % par_ax, *widget.JXY()) 

4396 

4397 if par_ax['zlabel']: 

4398 label_font = gmt.label_font() 

4399 label_font_size = gmt.label_font_size() 

4400 label_offset = zlabeloffset 

4401 gmt.pstext( 

4402 R=(0, 1, 0, 2), D="%gp/0p" % label_offset, 

4403 N=True, 

4404 in_rows=[(1, 1, label_font_size, -90, label_font, 'CB', 

4405 par_ax['zlabel'])], 

4406 *widget.JXY())