Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/gui/snuffler/marker.py: 84%
479 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-04 12:33 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-04 12:33 +0000
1# https://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6import math
7import copy
8import logging
9import sys
11import numpy as num
13from pyrocko import util, plot, model, trace
14from pyrocko.util import TableWriter, TableReader, gmtime_x, mystrftime
17logger = logging.getLogger('pyrocko.gui.snuffler.marker')
20if sys.version_info[0] >= 3:
21 polarity_symbols = {1: u'\u2191', -1: u'\u2193', None: u'', 0: u'\u2195'}
22else:
23 polarity_symbols = {1: '+', -1: '-', None: '', 0: '0'}
26def str_to_float_or_none(s):
27 if s == 'None':
28 return None
29 return float(s)
32def str_to_str_or_none(s):
33 if s == 'None':
34 return None
35 return s
38def str_to_int_or_none(s):
39 if s == 'None':
40 return None
41 return int(s)
44def str_to_bool(s):
45 return s.lower() in ('true', 't', '1')
48def myctime(timestamp):
49 tt, ms = gmtime_x(timestamp)
50 return mystrftime(None, tt, ms)
53g_color_b = [plot.color(x) for x in (
54 'scarletred1', 'scarletred2', 'scarletred3',
55 'chameleon1', 'chameleon2', 'chameleon3',
56 'skyblue1', 'skyblue2', 'skyblue3',
57 'orange1', 'orange2', 'orange3',
58 'plum1', 'plum2', 'plum3',
59 'chocolate1', 'chocolate2', 'chocolate3',
60 'butter1', 'butter2', 'butter3',
61 'aluminium3', 'aluminium4', 'aluminium5')]
64class MarkerParseError(Exception):
65 pass
68class MarkerOneNSLCRequired(Exception):
69 '''
70 Raised when a marker with exactly one NSLC entry is required but there are
71 zero or more than one.
72 '''
73 pass
76class Marker(object):
77 '''
78 General purpose marker GUI element and base class for
79 :py:class:`EventMarker` and :py:class:`PhaseMarker`.
81 :param nslc_ids: list of (network, station, location, channel) tuples
82 (may contain wildcards)
83 :param tmin: start time
84 :param tmax: end time
85 :param kind: (optional) integer to distinguish groups of markers
86 (color-coded)
87 '''
89 @staticmethod
90 def save_markers(markers, fn, fdigits=3):
91 '''
92 Static method to write marker objects to file.
94 :param markers: list of :py:class:`Marker` objects
95 :param fn: filename as string
96 :param fdigits: number of decimal digits to use for sub-second time
97 strings (default 3)
98 '''
99 f = open(fn, 'w')
100 f.write('# Snuffler Markers File Version 0.2\n')
101 writer = TableWriter(f)
102 for marker in markers:
103 a = marker.get_attributes(fdigits=fdigits)
104 w = marker.get_attribute_widths(fdigits=fdigits)
105 row = []
106 for x in a:
107 if x is None or x == '':
108 row.append('None')
109 else:
110 row.append(x)
112 writer.writerow(row, w)
114 f.close()
116 @staticmethod
117 def load_markers(fn):
118 '''
119 Static method to load markers from file.
121 :param filename: filename as string
122 :returns: list of :py:class:`Marker`, :py:class:`EventMarker` or
123 :py:class:`PhaseMarker` objects
124 '''
125 markers = []
126 with open(fn, 'r') as f:
127 line = f.readline()
128 if not line.startswith('# Snuffler Markers File Version'):
129 raise MarkerParseError('Not a marker file')
131 elif line.startswith('# Snuffler Markers File Version 0.2'):
132 reader = TableReader(f)
133 while not reader.eof:
134 row = reader.readrow()
135 if not row:
136 continue
137 if row[0] == 'event:':
138 marker = EventMarker.from_attributes(row)
139 elif row[0] == 'phase:':
140 marker = PhaseMarker.from_attributes(row)
141 else:
142 marker = Marker.from_attributes(row)
144 markers.append(marker)
145 else:
146 logger.warning('Unsupported Markers File Version')
148 return markers
150 def __init__(self, nslc_ids, tmin, tmax, kind=0):
151 self.set(nslc_ids, tmin, tmax)
152 self.alerted = False
153 self.selected = False
154 self.kind = kind
155 self.active = False
157 def set(self, nslc_ids, tmin, tmax):
158 '''
159 Set ``nslc_ids``, start time and end time of :py:class:`Marker`.
161 :param nslc_ids: list or set of (network, station, location, channel)
162 tuples
163 :param tmin: start time
164 :param tmax: end time
165 '''
166 self.nslc_ids = nslc_ids
167 self.tmin = util.to_time_float(tmin)
168 self.tmax = util.to_time_float(tmax)
170 def set_kind(self, kind):
171 '''
172 Set kind of :py:class:`Marker`.
174 :param kind: (optional) integer to distinguish groups of markers
175 (color-coded)
176 '''
177 self.kind = kind
179 def get_tmin(self):
180 '''
181 Get *start time* of :py:class:`Marker`.
182 '''
183 return self.tmin
185 def get_tmax(self):
186 '''
187 Get *end time* of :py:class:`Marker`.
188 '''
189 return self.tmax
191 def get_nslc_ids(self):
192 '''
193 Get marker's network-station-location-channel pattern.
195 :returns: list or set of (network, station, location, channel) tuples
197 The network, station, location, or channel strings may contain wildcard
198 expressions.
199 '''
200 return self.nslc_ids
202 def is_alerted(self):
203 return self.alerted
205 def is_selected(self):
206 return self.selected
208 def set_alerted(self, state):
209 self.alerted = state
211 def match_nsl(self, nsl):
212 '''
213 See documentation of :py:func:`pyrocko.util.match_nslc`.
214 '''
215 patterns = ['.'.join(x[:3]) for x in self.nslc_ids]
216 return util.match_nslc(patterns, nsl)
218 def match_nslc(self, nslc):
219 '''
220 See documentation of :py:func:`pyrocko.util.match_nslc`.
221 '''
222 patterns = ['.'.join(x) for x in self.nslc_ids]
223 return util.match_nslc(patterns, nslc)
225 def one_nslc(self):
226 '''
227 Get single NSLC pattern or raise an exception if there is not exactly
228 one.
230 If *nslc_ids* contains a single entry, return it. If more than one is
231 available, raise :py:exc:`MarkerOneNSLCRequired`.
232 '''
233 if len(self.nslc_ids) != 1:
234 raise MarkerOneNSLCRequired()
236 return list(self.nslc_ids)[0]
238 def hoover_message(self):
239 return ''
241 def copy(self):
242 '''
243 Get a copy of this marker.
244 '''
245 return copy.deepcopy(self)
247 def __str__(self):
248 traces = ','.join(['.'.join(nslc_id) for nslc_id in self.nslc_ids])
249 st = myctime
250 if self.tmin == self.tmax:
251 return '%s %i %s' % (st(self.tmin), self.kind, traces)
252 else:
253 return '%s %s %g %i %s' % (
254 st(self.tmin), st(self.tmax), self.tmax-self.tmin, self.kind,
255 traces)
257 def get_attributes(self, fdigits=3):
258 traces = ','.join(['.'.join(nslc_id) for nslc_id in self.nslc_ids])
260 def st(t):
261 return util.time_to_str(
262 t, format='%Y-%m-%d %H:%M:%S.'+'%iFRAC' % fdigits)
264 vals = []
265 vals.extend(st(self.tmin).split())
266 if self.tmin != self.tmax:
267 vals.extend(st(self.tmax).split())
268 vals.append(self.tmax-self.tmin)
270 vals.append(self.kind)
271 vals.append(traces)
272 return vals
274 def get_attribute_widths(self, fdigits=3):
275 ws = [10, 9+fdigits]
276 if self.tmin != self.tmax:
277 ws.extend([10, 9+fdigits, 12])
278 ws.extend([2, 15])
279 return ws
281 @staticmethod
282 def parse_attributes(vals):
283 tmin = util.str_to_time(vals[0] + ' ' + vals[1])
284 i = 2
285 tmax = tmin
286 if len(vals) == 7:
287 tmax = util.str_to_time(vals[2] + ' ' + vals[3])
288 i = 5
290 kind = int(vals[i])
291 traces = vals[i+1]
292 if traces == 'None':
293 nslc_ids = []
294 else:
295 nslc_ids = tuple(
296 [tuple(nslc_id.split('.')) for nslc_id in traces.split(',')])
298 return nslc_ids, tmin, tmax, kind
300 @staticmethod
301 def from_attributes(vals):
302 return Marker(*Marker.parse_attributes(vals))
304 def select_color(self, colorlist):
306 def cl(x):
307 return colorlist[(self.kind*3+x) % len(colorlist)]
309 if self.selected:
310 return cl(1)
312 if self.alerted:
313 return cl(1)
315 return cl(2)
317 def draw(
318 self, p, time_projection, y_projection,
319 draw_line=True,
320 draw_triangle=False,
321 **kwargs):
323 from ..qt_compat import qc, qg
324 from .. import util as gui_util
326 color = self.select_color(g_color_b)
327 pen = qg.QPen(qg.QColor(*color))
328 pen.setWidth(2)
329 p.setPen(pen)
331 umin = time_projection(self.tmin)
332 umax = time_projection(self.tmax)
333 v0, v1 = y_projection.get_out_range()
334 line = qc.QLineF(umin-1, v0, umax+1, v0)
335 p.drawLine(line)
337 if self.selected or self.alerted or not self.nslc_ids:
338 linepen = qg.QPen(pen)
339 if self.selected or self.alerted:
340 linepen.setStyle(qc.Qt.CustomDashLine)
341 pat = [5., 3.]
342 linepen.setDashPattern(pat)
343 if self.alerted and not self.selected:
344 linepen.setColor(qg.QColor(150, 150, 150))
346 s = 9.
347 utriangle = gui_util.make_QPolygonF(
348 [-0.577*s, 0., 0.577*s], [0., 1.*s, 0.])
349 ltriangle = gui_util.make_QPolygonF(
350 [-0.577*s, 0., 0.577*s], [0., -1.*s, 0.])
352 def drawline(t):
353 u = time_projection(t)
354 line = qc.QLineF(u, v0, u, v1)
355 p.drawLine(line)
357 def drawtriangles(t):
358 u = time_projection(t)
359 t = qg.QPolygonF(utriangle)
360 t.translate(u, v0)
361 p.drawConvexPolygon(t)
362 t = qg.QPolygonF(ltriangle)
363 t.translate(u, v1)
364 p.drawConvexPolygon(t)
366 if draw_line or self.selected or self.alerted:
367 p.setPen(linepen)
368 drawline(self.tmin)
369 drawline(self.tmax)
371 if draw_triangle:
372 pen.setStyle(qc.Qt.SolidLine)
373 pen.setJoinStyle(qc.Qt.MiterJoin)
374 pen.setWidth(2)
375 p.setPen(pen)
376 p.setBrush(qg.QColor(*color))
377 drawtriangles(self.tmin)
379 def draw_trace(
380 self, viewer, p, tr, time_projection, track_projection, gain,
381 outline_label=False):
383 from ..qt_compat import qc, qg
384 from .. import util as gui_util
386 if self.nslc_ids and not self.match_nslc(tr.nslc_id):
387 return
389 color = self.select_color(g_color_b)
390 pen = qg.QPen(qg.QColor(*color))
391 pen.setWidth(2)
392 p.setPen(pen)
393 p.setBrush(qc.Qt.NoBrush)
395 def drawpoint(t, y):
396 u = time_projection(t)
397 v = track_projection(y)
398 rect = qc.QRectF(u-2, v-2, 4, 4)
399 p.drawRect(rect)
401 def drawline(t):
402 u = time_projection(t)
403 v0, v1 = track_projection.get_out_range()
404 line = qc.QLineF(u, v0, u, v1)
405 p.drawLine(line)
407 try:
408 snippet = tr.chop(
409 self.tmin, self.tmax,
410 inplace=False,
411 include_last=True,
412 snap=(math.ceil, math.floor))
414 vdata = track_projection(gain*snippet.get_ydata())
415 udata_min = float(
416 time_projection(snippet.tmin))
417 udata_max = float(
418 time_projection(snippet.tmin+snippet.deltat*(vdata.size-1)))
419 udata = num.linspace(udata_min, udata_max, vdata.size)
420 qpoints = gui_util.make_QPolygonF(udata, vdata)
421 pen.setWidth(1)
422 p.setPen(pen)
423 p.drawPolyline(qpoints)
424 pen.setWidth(2)
425 p.setPen(pen)
426 drawpoint(*tr(self.tmin, clip=True, snap=math.ceil))
427 drawpoint(*tr(self.tmax, clip=True, snap=math.floor))
429 except trace.NoData:
430 pass
432 color = self.select_color(g_color_b)
433 pen = qg.QPen(qg.QColor(*color))
434 pen.setWidth(2)
435 p.setPen(pen)
437 drawline(self.tmin)
438 drawline(self.tmax)
440 label = self.get_label()
441 if label:
442 label_bg = qg.QBrush(qg.QColor(255, 255, 255))
444 u = time_projection(self.tmin)
445 v0, v1 = track_projection.get_out_range()
446 if outline_label:
447 du = -7
448 else:
449 du = -5
450 gui_util.draw_label(
451 p, u+du, v0, label, label_bg, 'TR',
452 outline=outline_label)
454 if self.tmin == self.tmax:
455 try:
456 drawpoint(self.tmin, tr.interpolate(self.tmin))
458 except IndexError:
459 pass
461 def get_label(self):
462 return None
464 def convert_to_phase_marker(
465 self,
466 event=None,
467 phasename=None,
468 polarity=None,
469 automatic=None,
470 incidence_angle=None,
471 takeoff_angle=None):
473 if isinstance(self, PhaseMarker):
474 return
476 self.__class__ = PhaseMarker
477 self._event = event
478 self._phasename = phasename
479 self._polarity = polarity
480 self._automatic = automatic
481 self._incidence_angle = incidence_angle
482 self._takeoff_angle = takeoff_angle
483 if self._event:
484 self._event_hash = event.get_hash()
485 self._event_time = event.time
486 else:
487 self._event_hash = None
488 self._event_time = None
489 self.active = False
491 def convert_to_event_marker(self, lat=0., lon=0.):
492 if isinstance(self, EventMarker):
493 return
495 if isinstance(self, PhaseMarker):
496 self.convert_to_marker()
498 self.__class__ = EventMarker
499 self._event = model.Event(lat, lon, time=self.tmin, name='Event')
500 self._event_hash = self._event.get_hash()
501 self.active = False
502 self.tmax = self.tmin
503 self.nslc_ids = []
506class EventMarker(Marker):
507 '''
508 GUI element representing a seismological event.
510 :param event: A :py:class:`~pyrocko.model.event.Event` object containing
511 meta information of a seismological event
512 :param kind: (optional) integer to distinguish groups of markers
513 :param event_hash: (optional) hash code of event (see:
514 :py:meth:`~pyrocko.model.event.Event.get_hash`)
515 '''
517 def __init__(self, event, kind=0, event_hash=None):
518 Marker.__init__(self, [], event.time, event.time, kind)
519 self._event = event
520 self.active = False
521 self._event_hash = event_hash
523 def get_event_hash(self):
524 if self._event_hash is not None:
525 return self._event_hash
526 else:
527 return self._event.get_hash()
529 def label(self):
530 t = []
531 mag = self._event.magnitude
532 if mag is not None:
533 t.append('M%3.1f' % mag)
535 reg = self._event.region
536 if reg is not None:
537 t.append(reg)
539 nam = self._event.name
540 if nam is not None:
541 t.append(nam)
543 s = ' '.join(t)
544 if not s:
545 s = '(Event)'
546 return s
548 def draw(self, p, time_projection, y_projection, with_label=False):
549 Marker.draw(
550 self, p, time_projection, y_projection,
551 draw_line=False,
552 draw_triangle=True)
554 if with_label:
555 self.draw_label(p, time_projection, y_projection)
557 def draw_label(self, p, time_projection, y_projection):
558 from ..qt_compat import qg
559 from .. import util as gui_util
561 u = time_projection(self.tmin)
562 v0, v1 = y_projection.get_out_range()
563 label_bg = qg.QBrush(qg.QColor(255, 255, 255))
564 gui_util.draw_label(
565 p, u, v0-10., self.label(), label_bg, 'CB',
566 outline=self.active)
568 def get_event(self):
569 '''
570 Return an instance of the :py:class:`~pyrocko.model.event.Event`
571 associated to this :py:class:`EventMarker`
572 '''
573 return self._event
575 def draw_trace(self, viewer, p, tr, time_projection, track_projection,
576 gain):
577 pass
579 def hoover_message(self):
580 ev = self.get_event()
581 evs = []
582 for k in 'magnitude lat lon depth name region catalog'.split():
583 if ev.__dict__[k] is not None and ev.__dict__[k] != '':
584 if k == 'depth':
585 sv = '%g km' % (ev.depth * 0.001)
586 else:
587 sv = '%s' % ev.__dict__[k]
588 evs.append('%s = %s' % (k, sv))
590 return ', '.join(evs)
592 def get_attributes(self, fdigits=3):
593 attributes = ['event:']
594 attributes.extend(Marker.get_attributes(self, fdigits=fdigits))
595 del attributes[-1]
596 e = self._event
597 attributes.extend([
598 e.get_hash(), e.lat, e.lon, e.depth, e.magnitude, e.catalog,
599 e.name, e.region])
601 return attributes
603 def get_attribute_widths(self, fdigits=3):
604 ws = [6]
605 ws.extend(Marker.get_attribute_widths(self, fdigits=fdigits))
606 del ws[-1]
607 ws.extend([14, 12, 12, 12, 4, 5, 0, 0])
608 return ws
610 @staticmethod
611 def from_attributes(vals):
613 nslc_ids, tmin, tmax, kind = Marker.parse_attributes(
614 vals[1:] + ['None'])
615 lat, lon, depth, magnitude = [
616 str_to_float_or_none(x) for x in vals[5:9]]
617 catalog, name, region = [
618 str_to_str_or_none(x) for x in vals[9:]]
619 e = model.Event(
620 lat, lon, time=tmin, name=name, depth=depth, magnitude=magnitude,
621 region=region, catalog=catalog)
622 marker = EventMarker(
623 e, kind, event_hash=str_to_str_or_none(vals[4]))
624 return marker
627class PhaseMarker(Marker):
628 '''
629 A PhaseMarker is a GUI-element representing a seismological phase arrival
631 :param nslc_ids: list of (network, station, location, channel) tuples (may
632 contain wildcards)
633 :param tmin: start time
634 :param tmax: end time
635 :param kind: (optional) integer to distinguish groups of markers
636 (color-coded)
637 :param event: a :py:class:`~pyrocko.model.event.Event` object containing
638 meta information of a seismological event
639 :param event_hash: (optional) hash code of event (see:
640 :py:meth:`~pyrocko.model.event.Event.get_hash`)
641 :param event_time: (optional) time of the associated event
642 :param phasename: (optional) name of the phase associated with the marker
643 :param polarity: (optional) polarity of arriving phase
644 :param automatic: (optional)
645 :param incident_angle: (optional) incident angle of phase
646 :param takeoff_angle: (optional) take off angle of phase
647 '''
648 def __init__(
649 self, nslc_ids, tmin, tmax,
650 kind=0,
651 event=None,
652 event_hash=None,
653 event_time=None,
654 phasename=None,
655 polarity=None,
656 automatic=None,
657 incidence_angle=None,
658 takeoff_angle=None):
660 Marker.__init__(self, nslc_ids, tmin, tmax, kind)
661 self._event = event
662 self._event_hash = event_hash
663 self._event_time = event_time
664 self._phasename = phasename
665 self._automatic = automatic
666 self._incidence_angle = incidence_angle
667 self._takeoff_angle = takeoff_angle
669 self.set_polarity(polarity)
671 def draw_trace(self, viewer, p, tr, time_projection, track_projection,
672 gain):
674 Marker.draw_trace(
675 self, viewer, p, tr, time_projection, track_projection, gain,
676 outline_label=(
677 self._event is not None and
678 self._event == viewer.get_active_event()))
680 def get_label(self):
681 t = []
682 if self._phasename is not None:
683 t.append(self._phasename)
684 if self._polarity is not None:
685 t.append(self.get_polarity_symbol())
687 if self._automatic:
688 t.append('@')
690 return ''.join(t)
692 def get_event(self):
693 '''
694 Return an instance of the :py:class:`~pyrocko.model.event.Event`
695 associated to this :py:class:`EventMarker`
696 '''
697 return self._event
699 def get_event_hash(self):
700 if self._event_hash is not None:
701 return self._event_hash
702 else:
703 if self._event is None:
704 return None
705 else:
706 return self._event.get_hash()
708 def get_event_time(self):
709 if self._event is not None:
710 return self._event.time
711 else:
712 return self._event_time
714 def set_event_hash(self, event_hash):
715 self._event_hash = event_hash
717 def set_event(self, event):
718 self._event = event
719 if event is not None:
720 self.set_event_hash(event.get_hash())
722 def get_phasename(self):
723 return self._phasename
725 def set_phasename(self, phasename):
726 self._phasename = phasename
728 def set_polarity(self, polarity):
729 if polarity not in [1, -1, 0, None]:
730 raise ValueError('polarity has to be 1, -1, 0 or None')
731 self._polarity = polarity
733 def get_polarity_symbol(self):
734 return polarity_symbols.get(self._polarity, '')
736 def get_polarity(self):
737 return self._polarity
739 def convert_to_marker(self):
740 del self._event
741 del self._event_hash
742 del self._phasename
743 del self._polarity
744 del self._automatic
745 del self._incidence_angle
746 del self._takeoff_angle
747 self.active = False
748 self.__class__ = Marker
750 def hoover_message(self):
751 toks = []
752 for k in 'incidence_angle takeoff_angle polarity'.split():
753 v = getattr(self, '_' + k)
754 if v is not None:
755 toks.append('%s = %s' % (k, v))
757 return ', '.join(toks)
759 def get_attributes(self, fdigits=3):
760 attributes = ['phase:']
761 attributes.extend(Marker.get_attributes(self, fdigits=fdigits))
763 et = None, None
764 if self._event:
765 et = self._st(self._event.time, fdigits).split()
766 elif self._event_time:
767 et = self._st(self._event_time, fdigits).split()
769 attributes.extend([
770 self.get_event_hash(), et[0], et[1], self._phasename,
771 self._polarity, self._automatic])
773 return attributes
775 def _st(self, t, fdigits):
776 return util.time_to_str(
777 t, format='%Y-%m-%d %H:%M:%S.'+'%iFRAC' % fdigits)
779 def get_attribute_widths(self, fdigits=3):
780 ws = [6]
781 ws.extend(Marker.get_attribute_widths(self, fdigits=fdigits))
782 ws.extend([14, 12, 12, 8, 4, 5])
783 return ws
785 @staticmethod
786 def from_attributes(vals):
787 if len(vals) == 14:
788 nbasicvals = 7
789 else:
790 nbasicvals = 4
791 nslc_ids, tmin, tmax, kind = Marker.parse_attributes(
792 vals[1:1+nbasicvals])
794 i = 8
795 if len(vals) == 14:
796 i = 11
798 event_hash = str_to_str_or_none(vals[i-3])
799 event_sdate = str_to_str_or_none(vals[i-2])
800 event_stime = str_to_str_or_none(vals[i-1])
802 if event_sdate is not None and event_stime is not None:
803 event_time = util.str_to_time(event_sdate + ' ' + event_stime)
804 else:
805 event_time = None
807 phasename = str_to_str_or_none(vals[i])
808 polarity = str_to_int_or_none(vals[i+1])
809 automatic = str_to_bool(vals[i+2])
810 marker = PhaseMarker(nslc_ids, tmin, tmax, kind, event=None,
811 event_hash=event_hash, event_time=event_time,
812 phasename=phasename, polarity=polarity,
813 automatic=automatic)
814 return marker
817def load_markers(filename):
818 '''
819 Load markers from file.
821 :param filename: filename as string
822 :returns: list of :py:class:`Marker` Objects
823 '''
825 return Marker.load_markers(filename)
828def save_markers(markers, filename, fdigits=3):
829 '''
830 Save markers to file.
832 :param markers: list of :py:class:`Marker` Objects
833 :param filename: filename as string
834 :param fdigits: number of decimal digits to use for sub-second time strings
835 '''
837 return Marker.save_markers(markers, filename, fdigits=fdigits)
840def associate_phases_to_events(markers):
841 '''
842 Reassociate phases to events after import from markers file.
843 '''
845 hash_to_events = {}
846 time_to_events = {}
847 for marker in markers:
848 if isinstance(marker, EventMarker):
849 ev = marker.get_event()
850 hash_to_events[marker.get_event_hash()] = ev
851 time_to_events[ev.time] = ev
853 for marker in markers:
854 if isinstance(marker, PhaseMarker):
855 h = marker.get_event_hash()
856 t = marker.get_event_time()
857 if marker.get_event() is None:
858 if h is not None and h in hash_to_events:
859 marker.set_event(hash_to_events[h])
860 marker.set_event_hash(None)
861 elif t is not None and t in time_to_events:
862 marker.set_event(time_to_events[t])
863 marker.set_event_hash(None)