1# http://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

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

5from __future__ import absolute_import 

6 

7import math 

8import copy 

9import logging 

10import sys 

11 

12import numpy as num 

13 

14from pyrocko import util, plot, model, trace 

15from pyrocko.util import TableWriter, TableReader, gmtime_x, mystrftime 

16 

17 

18logger = logging.getLogger('pyrocko.gui.marker') 

19 

20 

21if sys.version_info[0] >= 3: 

22 polarity_symbols = {1: u'\u2191', -1: u'\u2193', None: u'', 0: u'\u2195'} 

23else: 

24 polarity_symbols = {1: '+', -1: '-', None: '', 0: '0'} 

25 

26 

27def str_to_float_or_none(s): 

28 if s == 'None': 

29 return None 

30 return float(s) 

31 

32 

33def str_to_str_or_none(s): 

34 if s == 'None': 

35 return None 

36 return s 

37 

38 

39def str_to_int_or_none(s): 

40 if s == 'None': 

41 return None 

42 return int(s) 

43 

44 

45def str_to_bool(s): 

46 return s.lower() in ('true', 't', '1') 

47 

48 

49def myctime(timestamp): 

50 tt, ms = gmtime_x(timestamp) 

51 return mystrftime(None, tt, ms) 

52 

53 

54g_color_b = [plot.color(x) for x in ( 

55 'scarletred1', 'scarletred2', 'scarletred3', 

56 'chameleon1', 'chameleon2', 'chameleon3', 

57 'skyblue1', 'skyblue2', 'skyblue3', 

58 'orange1', 'orange2', 'orange3', 

59 'plum1', 'plum2', 'plum3', 

60 'chocolate1', 'chocolate2', 'chocolate3')] 

61 

62 

63class MarkerParseError(Exception): 

64 pass 

65 

66 

67class MarkerOneNSLCRequired(Exception): 

68 pass 

69 

70 

71class Marker(object): 

72 ''' 

73 General purpose marker GUI element and base class for 

74 :py:class:`EventMarker` and :py:class:`PhaseMarker`. 

75 

76 :param nslc_ids: list of (network, station, location, channel) tuples 

77 (may contain wildcards) 

78 :param tmin: start time 

79 :param tmax: end time 

80 :param kind: (optional) integer to distinguish groups of markers 

81 (color-coded) 

82 ''' 

83 

84 @staticmethod 

85 def save_markers(markers, fn, fdigits=3): 

86 ''' 

87 Static method to write marker objects to file. 

88 

89 :param markers: list of :py:class:`Marker` objects 

90 :param fn: filename as string 

91 :param fdigits: number of decimal digits to use for sub-second time 

92 strings (default 3) 

93 ''' 

94 f = open(fn, 'w') 

95 f.write('# Snuffler Markers File Version 0.2\n') 

96 writer = TableWriter(f) 

97 for marker in markers: 

98 a = marker.get_attributes(fdigits=fdigits) 

99 w = marker.get_attribute_widths(fdigits=fdigits) 

100 row = [] 

101 for x in a: 

102 if x is None or x == '': 

103 row.append('None') 

104 else: 

105 row.append(x) 

106 

107 writer.writerow(row, w) 

108 

109 f.close() 

110 

111 @staticmethod 

112 def load_markers(fn): 

113 ''' 

114 Static method to load markers from file. 

115 

116 :param filename: filename as string 

117 :returns: list of :py:class:`Marker`, :py:class:`EventMarker` or 

118 :py:class:`PhaseMarker` objects 

119 ''' 

120 markers = [] 

121 with open(fn, 'r') as f: 

122 line = f.readline() 

123 if not line.startswith('# Snuffler Markers File Version'): 

124 raise MarkerParseError('Not a marker file') 

125 

126 elif line.startswith('# Snuffler Markers File Version 0.2'): 

127 reader = TableReader(f) 

128 while not reader.eof: 

129 row = reader.readrow() 

130 if not row: 

131 continue 

132 if row[0] == 'event:': 

133 marker = EventMarker.from_attributes(row) 

134 elif row[0] == 'phase:': 

135 marker = PhaseMarker.from_attributes(row) 

136 else: 

137 marker = Marker.from_attributes(row) 

138 

139 markers.append(marker) 

140 else: 

141 logger.warning('Unsupported Markers File Version') 

142 

143 return markers 

144 

145 def __init__(self, nslc_ids, tmin, tmax, kind=0): 

146 self.set(nslc_ids, tmin, tmax) 

147 self.alerted = False 

148 self.selected = False 

149 self.kind = kind 

150 self.active = False 

151 

152 def set(self, nslc_ids, tmin, tmax): 

153 ''' 

154 Set ``nslc_ids``, start time and end time of :py:class:`Marker`. 

155 

156 :param nslc_ids: list or set of (network, station, location, channel) 

157 tuples 

158 :param tmin: start time 

159 :param tmax: end time 

160 ''' 

161 self.nslc_ids = nslc_ids 

162 self.tmin = util.to_time_float(tmin) 

163 self.tmax = util.to_time_float(tmax) 

164 

165 def set_kind(self, kind): 

166 ''' 

167 Set kind of :py:class:`Marker`. 

168 

169 :param kind: (optional) integer to distinguish groups of markers 

170 (color-coded) 

171 ''' 

172 self.kind = kind 

173 

174 def get_tmin(self): 

175 ''' 

176 Get *start time* of :py:class:`Marker`. 

177 ''' 

178 return self.tmin 

179 

180 def get_tmax(self): 

181 ''' 

182 Get *end time* of :py:class:`Marker`. 

183 ''' 

184 return self.tmax 

185 

186 def get_nslc_ids(self): 

187 ''' 

188 Get marker's network-station-location-channel pattern. 

189 

190 :returns: list or set of (network, station, location, channel) tuples 

191 

192 The network, station, location, or channel strings may contain wildcard 

193 expressions. 

194 ''' 

195 return self.nslc_ids 

196 

197 def is_alerted(self): 

198 return self.alerted 

199 

200 def is_selected(self): 

201 return self.selected 

202 

203 def set_alerted(self, state): 

204 self.alerted = state 

205 

206 def match_nsl(self, nsl): 

207 ''' 

208 See documentation of :py:func:`pyrocko.util.match_nslc`. 

209 ''' 

210 patterns = ['.'.join(x[:3]) for x in self.nslc_ids] 

211 return util.match_nslc(patterns, nsl) 

212 

213 def match_nslc(self, nslc): 

214 ''' 

215 See documentation of :py:func:`pyrocko.util.match_nslc`. 

216 ''' 

217 patterns = ['.'.join(x) for x in self.nslc_ids] 

218 return util.match_nslc(patterns, nslc) 

219 

220 def one_nslc(self): 

221 ''' 

222 If one *nslc_id* defines this marker return this id. 

223 If more than one *nslc_id* is defined in the :py:class:`Marker`s 

224 *nslc_ids* raise :py:exc:`MarkerOneNSLCRequired`. 

225 ''' 

226 if len(self.nslc_ids) != 1: 

227 raise MarkerOneNSLCRequired() 

228 

229 return list(self.nslc_ids)[0] 

230 

231 def hoover_message(self): 

232 return '' 

233 

234 def copy(self): 

235 ''' 

236 Get a copy of this marker. 

237 ''' 

238 return copy.deepcopy(self) 

239 

240 def __str__(self): 

241 traces = ','.join(['.'.join(nslc_id) for nslc_id in self.nslc_ids]) 

242 st = myctime 

243 if self.tmin == self.tmax: 

244 return '%s %i %s' % (st(self.tmin), self.kind, traces) 

245 else: 

246 return '%s %s %g %i %s' % ( 

247 st(self.tmin), st(self.tmax), self.tmax-self.tmin, self.kind, 

248 traces) 

249 

250 def get_attributes(self, fdigits=3): 

251 traces = ','.join(['.'.join(nslc_id) for nslc_id in self.nslc_ids]) 

252 

253 def st(t): 

254 return util.time_to_str( 

255 t, format='%Y-%m-%d %H:%M:%S.'+'%iFRAC' % fdigits) 

256 

257 vals = [] 

258 vals.extend(st(self.tmin).split()) 

259 if self.tmin != self.tmax: 

260 vals.extend(st(self.tmax).split()) 

261 vals.append(self.tmax-self.tmin) 

262 

263 vals.append(self.kind) 

264 vals.append(traces) 

265 return vals 

266 

267 def get_attribute_widths(self, fdigits=3): 

268 ws = [10, 9+fdigits] 

269 if self.tmin != self.tmax: 

270 ws.extend([10, 9+fdigits, 12]) 

271 ws.extend([2, 15]) 

272 return ws 

273 

274 @staticmethod 

275 def parse_attributes(vals): 

276 tmin = util.str_to_time(vals[0] + ' ' + vals[1]) 

277 i = 2 

278 tmax = tmin 

279 if len(vals) == 7: 

280 tmax = util.str_to_time(vals[2] + ' ' + vals[3]) 

281 i = 5 

282 

283 kind = int(vals[i]) 

284 traces = vals[i+1] 

285 if traces == 'None': 

286 nslc_ids = [] 

287 else: 

288 nslc_ids = tuple( 

289 [tuple(nslc_id.split('.')) for nslc_id in traces.split(',')]) 

290 

291 return nslc_ids, tmin, tmax, kind 

292 

293 @staticmethod 

294 def from_attributes(vals): 

295 return Marker(*Marker.parse_attributes(vals)) 

296 

297 def select_color(self, colorlist): 

298 

299 def cl(x): 

300 return colorlist[(self.kind*3+x) % len(colorlist)] 

301 

302 if self.selected: 

303 return cl(1) 

304 

305 if self.alerted: 

306 return cl(1) 

307 

308 return cl(2) 

309 

310 def draw( 

311 self, p, time_projection, y_projection, 

312 draw_line=True, 

313 draw_triangle=False, 

314 **kwargs): 

315 

316 from .qt_compat import qc, qg 

317 from . import util as gui_util 

318 

319 color = self.select_color(g_color_b) 

320 pen = qg.QPen(qg.QColor(*color)) 

321 pen.setWidth(2) 

322 p.setPen(pen) 

323 

324 umin = time_projection(self.tmin) 

325 umax = time_projection(self.tmax) 

326 v0, v1 = y_projection.get_out_range() 

327 line = qc.QLineF(umin-1, v0, umax+1, v0) 

328 p.drawLine(line) 

329 

330 if self.selected or self.alerted or not self.nslc_ids: 

331 linepen = qg.QPen(pen) 

332 if self.selected or self.alerted: 

333 linepen.setStyle(qc.Qt.CustomDashLine) 

334 pat = [5., 3.] 

335 linepen.setDashPattern(pat) 

336 if self.alerted and not self.selected: 

337 linepen.setColor(qg.QColor(150, 150, 150)) 

338 

339 s = 9. 

340 utriangle = gui_util.make_QPolygonF( 

341 [-0.577*s, 0., 0.577*s], [0., 1.*s, 0.]) 

342 ltriangle = gui_util.make_QPolygonF( 

343 [-0.577*s, 0., 0.577*s], [0., -1.*s, 0.]) 

344 

345 def drawline(t): 

346 u = time_projection(t) 

347 line = qc.QLineF(u, v0, u, v1) 

348 p.drawLine(line) 

349 

350 def drawtriangles(t): 

351 u = time_projection(t) 

352 t = qg.QPolygonF(utriangle) 

353 t.translate(u, v0) 

354 p.drawConvexPolygon(t) 

355 t = qg.QPolygonF(ltriangle) 

356 t.translate(u, v1) 

357 p.drawConvexPolygon(t) 

358 

359 if draw_line or self.selected or self.alerted: 

360 p.setPen(linepen) 

361 drawline(self.tmin) 

362 drawline(self.tmax) 

363 

364 if draw_triangle: 

365 pen.setStyle(qc.Qt.SolidLine) 

366 pen.setJoinStyle(qc.Qt.MiterJoin) 

367 pen.setWidth(2) 

368 p.setPen(pen) 

369 p.setBrush(qg.QColor(*color)) 

370 drawtriangles(self.tmin) 

371 

372 def draw_trace( 

373 self, viewer, p, tr, time_projection, track_projection, gain, 

374 outline_label=False): 

375 

376 from .qt_compat import qc, qg 

377 from . import util as gui_util 

378 

379 if self.nslc_ids and not self.match_nslc(tr.nslc_id): 

380 return 

381 

382 color = self.select_color(g_color_b) 

383 pen = qg.QPen(qg.QColor(*color)) 

384 pen.setWidth(2) 

385 p.setPen(pen) 

386 p.setBrush(qc.Qt.NoBrush) 

387 

388 def drawpoint(t, y): 

389 u = time_projection(t) 

390 v = track_projection(y) 

391 rect = qc.QRectF(u-2, v-2, 4, 4) 

392 p.drawRect(rect) 

393 

394 def drawline(t): 

395 u = time_projection(t) 

396 v0, v1 = track_projection.get_out_range() 

397 line = qc.QLineF(u, v0, u, v1) 

398 p.drawLine(line) 

399 

400 try: 

401 snippet = tr.chop( 

402 self.tmin, self.tmax, 

403 inplace=False, 

404 include_last=True, 

405 snap=(math.ceil, math.floor)) 

406 

407 vdata = track_projection(gain*snippet.get_ydata()) 

408 udata_min = float( 

409 time_projection(snippet.tmin)) 

410 udata_max = float( 

411 time_projection(snippet.tmin+snippet.deltat*(vdata.size-1))) 

412 udata = num.linspace(udata_min, udata_max, vdata.size) 

413 qpoints = gui_util.make_QPolygonF(udata, vdata) 

414 pen.setWidth(1) 

415 p.setPen(pen) 

416 p.drawPolyline(qpoints) 

417 pen.setWidth(2) 

418 p.setPen(pen) 

419 drawpoint(*tr(self.tmin, clip=True, snap=math.ceil)) 

420 drawpoint(*tr(self.tmax, clip=True, snap=math.floor)) 

421 

422 except trace.NoData: 

423 pass 

424 

425 color = self.select_color(g_color_b) 

426 pen = qg.QPen(qg.QColor(*color)) 

427 pen.setWidth(2) 

428 p.setPen(pen) 

429 

430 drawline(self.tmin) 

431 drawline(self.tmax) 

432 

433 label = self.get_label() 

434 if label: 

435 label_bg = qg.QBrush(qg.QColor(255, 255, 255)) 

436 

437 u = time_projection(self.tmin) 

438 v0, v1 = track_projection.get_out_range() 

439 if outline_label: 

440 du = -7 

441 else: 

442 du = -5 

443 gui_util.draw_label( 

444 p, u+du, v0, label, label_bg, 'TR', 

445 outline=outline_label) 

446 

447 if self.tmin == self.tmax: 

448 try: 

449 drawpoint(self.tmin, tr.interpolate(self.tmin)) 

450 

451 except IndexError: 

452 pass 

453 

454 def get_label(self): 

455 return None 

456 

457 def convert_to_phase_marker( 

458 self, 

459 event=None, 

460 phasename=None, 

461 polarity=None, 

462 automatic=None, 

463 incidence_angle=None, 

464 takeoff_angle=None): 

465 

466 if isinstance(self, PhaseMarker): 

467 return 

468 

469 self.__class__ = PhaseMarker 

470 self._event = event 

471 self._phasename = phasename 

472 self._polarity = polarity 

473 self._automatic = automatic 

474 self._incidence_angle = incidence_angle 

475 self._takeoff_angle = takeoff_angle 

476 if self._event: 

477 self._event_hash = event.get_hash() 

478 self._event_time = event.time 

479 else: 

480 self._event_hash = None 

481 self._event_time = None 

482 self.active = False 

483 

484 def convert_to_event_marker(self, lat=0., lon=0.): 

485 if isinstance(self, EventMarker): 

486 return 

487 

488 if isinstance(self, PhaseMarker): 

489 self.convert_to_marker() 

490 

491 self.__class__ = EventMarker 

492 self._event = model.Event(lat, lon, time=self.tmin, name='Event') 

493 self._event_hash = self._event.get_hash() 

494 self.active = False 

495 self.tmax = self.tmin 

496 self.nslc_ids = [] 

497 

498 

499class EventMarker(Marker): 

500 ''' 

501 GUI element representing a seismological event. 

502 

503 :param event: A :py:class:`pyrocko.model.Event` object containing meta 

504 information of a seismological event 

505 :param kind: (optional) integer to distinguish groups of markers 

506 :param event_hash: (optional) hash code of event (see: 

507 :py:meth:`pyrocko.model.Event.get_hash`) 

508 ''' 

509 

510 def __init__(self, event, kind=0, event_hash=None): 

511 Marker.__init__(self, [], event.time, event.time, kind) 

512 self._event = event 

513 self.active = False 

514 self._event_hash = event_hash 

515 

516 def get_event_hash(self): 

517 if self._event_hash is not None: 

518 return self._event_hash 

519 else: 

520 return self._event.get_hash() 

521 

522 def label(self): 

523 t = [] 

524 mag = self._event.magnitude 

525 if mag is not None: 

526 t.append('M%3.1f' % mag) 

527 

528 reg = self._event.region 

529 if reg is not None: 

530 t.append(reg) 

531 

532 nam = self._event.name 

533 if nam is not None: 

534 t.append(nam) 

535 

536 s = ' '.join(t) 

537 if not s: 

538 s = '(Event)' 

539 return s 

540 

541 def draw(self, p, time_projection, y_projection, with_label=False): 

542 Marker.draw( 

543 self, p, time_projection, y_projection, 

544 draw_line=False, 

545 draw_triangle=True) 

546 

547 if with_label: 

548 self.draw_label(p, time_projection, y_projection) 

549 

550 def draw_label(self, p, time_projection, y_projection): 

551 from .qt_compat import qg 

552 from . import util as gui_util 

553 

554 u = time_projection(self.tmin) 

555 v0, v1 = y_projection.get_out_range() 

556 label_bg = qg.QBrush(qg.QColor(255, 255, 255)) 

557 gui_util.draw_label( 

558 p, u, v0-10., self.label(), label_bg, 'CB', 

559 outline=self.active) 

560 

561 def get_event(self): 

562 ''' 

563 Return an instance of the :py:class:`pyrocko.model.Event` associated 

564 to this :py:class:`EventMarker` 

565 ''' 

566 return self._event 

567 

568 def draw_trace(self, viewer, p, tr, time_projection, track_projection, 

569 gain): 

570 pass 

571 

572 def hoover_message(self): 

573 ev = self.get_event() 

574 evs = [] 

575 for k in 'magnitude lat lon depth name region catalog'.split(): 

576 if ev.__dict__[k] is not None and ev.__dict__[k] != '': 

577 if k == 'depth': 

578 sv = '%g km' % (ev.depth * 0.001) 

579 else: 

580 sv = '%s' % ev.__dict__[k] 

581 evs.append('%s = %s' % (k, sv)) 

582 

583 return ', '.join(evs) 

584 

585 def get_attributes(self, fdigits=3): 

586 attributes = ['event:'] 

587 attributes.extend(Marker.get_attributes(self, fdigits=fdigits)) 

588 del attributes[-1] 

589 e = self._event 

590 attributes.extend([ 

591 e.get_hash(), e.lat, e.lon, e.depth, e.magnitude, e.catalog, 

592 e.name, e.region]) 

593 

594 return attributes 

595 

596 def get_attribute_widths(self, fdigits=3): 

597 ws = [6] 

598 ws.extend(Marker.get_attribute_widths(self, fdigits=fdigits)) 

599 del ws[-1] 

600 ws.extend([14, 12, 12, 12, 4, 5, 0, 0]) 

601 return ws 

602 

603 @staticmethod 

604 def from_attributes(vals): 

605 

606 nslc_ids, tmin, tmax, kind = Marker.parse_attributes( 

607 vals[1:] + ['None']) 

608 lat, lon, depth, magnitude = [ 

609 str_to_float_or_none(x) for x in vals[5:9]] 

610 catalog, name, region = [ 

611 str_to_str_or_none(x) for x in vals[9:]] 

612 e = model.Event( 

613 lat, lon, time=tmin, name=name, depth=depth, magnitude=magnitude, 

614 region=region, catalog=catalog) 

615 marker = EventMarker( 

616 e, kind, event_hash=str_to_str_or_none(vals[4])) 

617 return marker 

618 

619 

620class PhaseMarker(Marker): 

621 ''' 

622 A PhaseMarker is a GUI-element representing a seismological phase arrival 

623 

624 :param nslc_ids: list of (network, station, location, channel) tuples (may 

625 contain wildcards) 

626 :param tmin: start time 

627 :param tmax: end time 

628 :param kind: (optional) integer to distinguish groups of markers 

629 (color-coded) 

630 :param event: a :py:class:`pyrocko.model.Event` object containing meta 

631 information of a seismological event 

632 :param event_hash: (optional) hash code of event (see: 

633 :py:meth:`pyrocko.model.Event.get_hash`) 

634 :param event_time: (optional) time of the associated event 

635 :param phasename: (optional) name of the phase associated with the marker 

636 :param polarity: (optional) polarity of arriving phase 

637 :param automatic: (optional) 

638 :param incident_angle: (optional) incident angle of phase 

639 :param takeoff_angle: (optional) take off angle of phase 

640 ''' 

641 def __init__( 

642 self, nslc_ids, tmin, tmax, 

643 kind=0, 

644 event=None, 

645 event_hash=None, 

646 event_time=None, 

647 phasename=None, 

648 polarity=None, 

649 automatic=None, 

650 incidence_angle=None, 

651 takeoff_angle=None): 

652 

653 Marker.__init__(self, nslc_ids, tmin, tmax, kind) 

654 self._event = event 

655 self._event_hash = event_hash 

656 self._event_time = event_time 

657 self._phasename = phasename 

658 self._automatic = automatic 

659 self._incidence_angle = incidence_angle 

660 self._takeoff_angle = takeoff_angle 

661 

662 self.set_polarity(polarity) 

663 

664 def draw_trace(self, viewer, p, tr, time_projection, track_projection, 

665 gain): 

666 

667 Marker.draw_trace( 

668 self, viewer, p, tr, time_projection, track_projection, gain, 

669 outline_label=( 

670 self._event is not None and 

671 self._event == viewer.get_active_event())) 

672 

673 def get_label(self): 

674 t = [] 

675 if self._phasename is not None: 

676 t.append(self._phasename) 

677 if self._polarity is not None: 

678 t.append(self.get_polarity_symbol()) 

679 

680 if self._automatic: 

681 t.append('@') 

682 

683 return ''.join(t) 

684 

685 def get_event(self): 

686 ''' 

687 Return an instance of the :py:class:`pyrocko.model.Event` associated 

688 to this :py:class:`EventMarker` 

689 ''' 

690 return self._event 

691 

692 def get_event_hash(self): 

693 if self._event_hash is not None: 

694 return self._event_hash 

695 else: 

696 if self._event is None: 

697 return None 

698 else: 

699 return self._event.get_hash() 

700 

701 def get_event_time(self): 

702 if self._event is not None: 

703 return self._event.time 

704 else: 

705 return self._event_time 

706 

707 def set_event_hash(self, event_hash): 

708 self._event_hash = event_hash 

709 

710 def set_event(self, event): 

711 self._event = event 

712 if event is not None: 

713 self.set_event_hash(event.get_hash()) 

714 

715 def get_phasename(self): 

716 return self._phasename 

717 

718 def set_phasename(self, phasename): 

719 self._phasename = phasename 

720 

721 def set_polarity(self, polarity): 

722 if polarity not in [1, -1, 0, None]: 

723 raise ValueError('polarity has to be 1, -1, 0 or None') 

724 self._polarity = polarity 

725 

726 def get_polarity_symbol(self): 

727 return polarity_symbols.get(self._polarity, '') 

728 

729 def get_polarity(self): 

730 return self._polarity 

731 

732 def convert_to_marker(self): 

733 del self._event 

734 del self._event_hash 

735 del self._phasename 

736 del self._polarity 

737 del self._automatic 

738 del self._incidence_angle 

739 del self._takeoff_angle 

740 self.active = False 

741 self.__class__ = Marker 

742 

743 def hoover_message(self): 

744 toks = [] 

745 for k in 'incidence_angle takeoff_angle polarity'.split(): 

746 v = getattr(self, '_' + k) 

747 if v is not None: 

748 toks.append('%s = %s' % (k, v)) 

749 

750 return ', '.join(toks) 

751 

752 def get_attributes(self, fdigits=3): 

753 attributes = ['phase:'] 

754 attributes.extend(Marker.get_attributes(self, fdigits=fdigits)) 

755 

756 et = None, None 

757 if self._event: 

758 et = self._st(self._event.time, fdigits).split() 

759 elif self._event_time: 

760 et = self._st(self._event_time, fdigits).split() 

761 

762 attributes.extend([ 

763 self.get_event_hash(), et[0], et[1], self._phasename, 

764 self._polarity, self._automatic]) 

765 

766 return attributes 

767 

768 def _st(self, t, fdigits): 

769 return util.time_to_str( 

770 t, format='%Y-%m-%d %H:%M:%S.'+'%iFRAC' % fdigits) 

771 

772 def get_attribute_widths(self, fdigits=3): 

773 ws = [6] 

774 ws.extend(Marker.get_attribute_widths(self, fdigits=fdigits)) 

775 ws.extend([14, 12, 12, 8, 4, 5]) 

776 return ws 

777 

778 @staticmethod 

779 def from_attributes(vals): 

780 if len(vals) == 14: 

781 nbasicvals = 7 

782 else: 

783 nbasicvals = 4 

784 nslc_ids, tmin, tmax, kind = Marker.parse_attributes( 

785 vals[1:1+nbasicvals]) 

786 

787 i = 8 

788 if len(vals) == 14: 

789 i = 11 

790 

791 event_hash = str_to_str_or_none(vals[i-3]) 

792 event_sdate = str_to_str_or_none(vals[i-2]) 

793 event_stime = str_to_str_or_none(vals[i-1]) 

794 

795 if event_sdate is not None and event_stime is not None: 

796 event_time = util.str_to_time(event_sdate + ' ' + event_stime) 

797 else: 

798 event_time = None 

799 

800 phasename = str_to_str_or_none(vals[i]) 

801 polarity = str_to_int_or_none(vals[i+1]) 

802 automatic = str_to_bool(vals[i+2]) 

803 marker = PhaseMarker(nslc_ids, tmin, tmax, kind, event=None, 

804 event_hash=event_hash, event_time=event_time, 

805 phasename=phasename, polarity=polarity, 

806 automatic=automatic) 

807 return marker 

808 

809 

810def load_markers(filename): 

811 ''' 

812 Load markers from file. 

813 

814 :param filename: filename as string 

815 :returns: list of :py:class:`Marker` Objects 

816 ''' 

817 

818 return Marker.load_markers(filename) 

819 

820 

821def save_markers(markers, filename, fdigits=3): 

822 ''' 

823 Save markers to file. 

824 

825 :param markers: list of :py:class:`Marker` Objects 

826 :param filename: filename as string 

827 :param fdigits: number of decimal digits to use for sub-second time strings 

828 ''' 

829 

830 return Marker.save_markers(markers, filename, fdigits=fdigits) 

831 

832 

833def associate_phases_to_events(markers): 

834 ''' 

835 Reassociate phases to events after import from markers file. 

836 ''' 

837 

838 hash_to_events = {} 

839 time_to_events = {} 

840 for marker in markers: 

841 if isinstance(marker, EventMarker): 

842 ev = marker.get_event() 

843 hash_to_events[marker.get_event_hash()] = ev 

844 time_to_events[ev.time] = ev 

845 

846 for marker in markers: 

847 if isinstance(marker, PhaseMarker): 

848 h = marker.get_event_hash() 

849 t = marker.get_event_time() 

850 if marker.get_event() is None: 

851 if h is not None and h in hash_to_events: 

852 marker.set_event(hash_to_events[h]) 

853 marker.set_event_hash(None) 

854 elif t is not None and t in time_to_events: 

855 marker.set_event(time_to_events[t]) 

856 marker.set_event_hash(None)