1# http://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
5from __future__ import absolute_import
7import math
8import copy
9import logging
10import sys
12import numpy as num
14from pyrocko import util, plot, model, trace
15from pyrocko.util import TableWriter, TableReader, gmtime_x, mystrftime
18logger = logging.getLogger('pyrocko.gui.marker')
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'}
27def str_to_float_or_none(s):
28 if s == 'None':
29 return None
30 return float(s)
33def str_to_str_or_none(s):
34 if s == 'None':
35 return None
36 return s
39def str_to_int_or_none(s):
40 if s == 'None':
41 return None
42 return int(s)
45def str_to_bool(s):
46 return s.lower() in ('true', 't', '1')
49def myctime(timestamp):
50 tt, ms = gmtime_x(timestamp)
51 return mystrftime(None, tt, ms)
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')]
63class MarkerParseError(Exception):
64 pass
67class MarkerOneNSLCRequired(Exception):
68 pass
71class Marker(object):
72 '''
73 General purpose marker GUI element and base class for
74 :py:class:`EventMarker` and :py:class:`PhaseMarker`.
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 '''
84 @staticmethod
85 def save_markers(markers, fn, fdigits=3):
86 '''
87 Static method to write marker objects to file.
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)
107 writer.writerow(row, w)
109 f.close()
111 @staticmethod
112 def load_markers(fn):
113 '''
114 Static method to load markers from file.
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')
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)
139 markers.append(marker)
140 else:
141 logger.warning('Unsupported Markers File Version')
143 return markers
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
152 def set(self, nslc_ids, tmin, tmax):
153 '''
154 Set ``nslc_ids``, start time and end time of :py:class:`Marker`.
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)
165 def set_kind(self, kind):
166 '''
167 Set kind of :py:class:`Marker`.
169 :param kind: (optional) integer to distinguish groups of markers
170 (color-coded)
171 '''
172 self.kind = kind
174 def get_tmin(self):
175 '''
176 Get *start time* of :py:class:`Marker`.
177 '''
178 return self.tmin
180 def get_tmax(self):
181 '''
182 Get *end time* of :py:class:`Marker`.
183 '''
184 return self.tmax
186 def get_nslc_ids(self):
187 '''
188 Get marker's network-station-location-channel pattern.
190 :returns: list or set of (network, station, location, channel) tuples
192 The network, station, location, or channel strings may contain wildcard
193 expressions.
194 '''
195 return self.nslc_ids
197 def is_alerted(self):
198 return self.alerted
200 def is_selected(self):
201 return self.selected
203 def set_alerted(self, state):
204 self.alerted = state
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)
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)
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()
229 return list(self.nslc_ids)[0]
231 def hoover_message(self):
232 return ''
234 def copy(self):
235 '''
236 Get a copy of this marker.
237 '''
238 return copy.deepcopy(self)
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)
250 def get_attributes(self, fdigits=3):
251 traces = ','.join(['.'.join(nslc_id) for nslc_id in self.nslc_ids])
253 def st(t):
254 return util.time_to_str(
255 t, format='%Y-%m-%d %H:%M:%S.'+'%iFRAC' % fdigits)
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)
263 vals.append(self.kind)
264 vals.append(traces)
265 return vals
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
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
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(',')])
291 return nslc_ids, tmin, tmax, kind
293 @staticmethod
294 def from_attributes(vals):
295 return Marker(*Marker.parse_attributes(vals))
297 def select_color(self, colorlist):
299 def cl(x):
300 return colorlist[(self.kind*3+x) % len(colorlist)]
302 if self.selected:
303 return cl(1)
305 if self.alerted:
306 return cl(1)
308 return cl(2)
310 def draw(
311 self, p, time_projection, y_projection,
312 draw_line=True,
313 draw_triangle=False,
314 **kwargs):
316 from .qt_compat import qc, qg
317 from . import util as gui_util
319 color = self.select_color(g_color_b)
320 pen = qg.QPen(qg.QColor(*color))
321 pen.setWidth(2)
322 p.setPen(pen)
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)
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))
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.])
345 def drawline(t):
346 u = time_projection(t)
347 line = qc.QLineF(u, v0, u, v1)
348 p.drawLine(line)
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)
359 if draw_line or self.selected or self.alerted:
360 p.setPen(linepen)
361 drawline(self.tmin)
362 drawline(self.tmax)
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)
372 def draw_trace(
373 self, viewer, p, tr, time_projection, track_projection, gain,
374 outline_label=False):
376 from .qt_compat import qc, qg
377 from . import util as gui_util
379 if self.nslc_ids and not self.match_nslc(tr.nslc_id):
380 return
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)
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)
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)
400 try:
401 snippet = tr.chop(
402 self.tmin, self.tmax,
403 inplace=False,
404 include_last=True,
405 snap=(math.ceil, math.floor))
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))
422 except trace.NoData:
423 pass
425 color = self.select_color(g_color_b)
426 pen = qg.QPen(qg.QColor(*color))
427 pen.setWidth(2)
428 p.setPen(pen)
430 drawline(self.tmin)
431 drawline(self.tmax)
433 label = self.get_label()
434 if label:
435 label_bg = qg.QBrush(qg.QColor(255, 255, 255))
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)
447 if self.tmin == self.tmax:
448 try:
449 drawpoint(self.tmin, tr.interpolate(self.tmin))
451 except IndexError:
452 pass
454 def get_label(self):
455 return None
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):
466 if isinstance(self, PhaseMarker):
467 return
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
484 def convert_to_event_marker(self, lat=0., lon=0.):
485 if isinstance(self, EventMarker):
486 return
488 if isinstance(self, PhaseMarker):
489 self.convert_to_marker()
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 = []
499class EventMarker(Marker):
500 '''
501 GUI element representing a seismological event.
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 '''
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
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()
522 def label(self):
523 t = []
524 mag = self._event.magnitude
525 if mag is not None:
526 t.append('M%3.1f' % mag)
528 reg = self._event.region
529 if reg is not None:
530 t.append(reg)
532 nam = self._event.name
533 if nam is not None:
534 t.append(nam)
536 s = ' '.join(t)
537 if not s:
538 s = '(Event)'
539 return s
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)
547 if with_label:
548 self.draw_label(p, time_projection, y_projection)
550 def draw_label(self, p, time_projection, y_projection):
551 from .qt_compat import qg
552 from . import util as gui_util
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)
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
568 def draw_trace(self, viewer, p, tr, time_projection, track_projection,
569 gain):
570 pass
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))
583 return ', '.join(evs)
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])
594 return attributes
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
603 @staticmethod
604 def from_attributes(vals):
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
620class PhaseMarker(Marker):
621 '''
622 A PhaseMarker is a GUI-element representing a seismological phase arrival
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):
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
662 self.set_polarity(polarity)
664 def draw_trace(self, viewer, p, tr, time_projection, track_projection,
665 gain):
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()))
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())
680 if self._automatic:
681 t.append('@')
683 return ''.join(t)
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
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()
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
707 def set_event_hash(self, event_hash):
708 self._event_hash = event_hash
710 def set_event(self, event):
711 self._event = event
712 if event is not None:
713 self.set_event_hash(event.get_hash())
715 def get_phasename(self):
716 return self._phasename
718 def set_phasename(self, phasename):
719 self._phasename = phasename
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
726 def get_polarity_symbol(self):
727 return polarity_symbols.get(self._polarity, '')
729 def get_polarity(self):
730 return self._polarity
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
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))
750 return ', '.join(toks)
752 def get_attributes(self, fdigits=3):
753 attributes = ['phase:']
754 attributes.extend(Marker.get_attributes(self, fdigits=fdigits))
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()
762 attributes.extend([
763 self.get_event_hash(), et[0], et[1], self._phasename,
764 self._polarity, self._automatic])
766 return attributes
768 def _st(self, t, fdigits):
769 return util.time_to_str(
770 t, format='%Y-%m-%d %H:%M:%S.'+'%iFRAC' % fdigits)
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
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])
787 i = 8
788 if len(vals) == 14:
789 i = 11
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])
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
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
810def load_markers(filename):
811 '''
812 Load markers from file.
814 :param filename: filename as string
815 :returns: list of :py:class:`Marker` Objects
816 '''
818 return Marker.load_markers(filename)
821def save_markers(markers, filename, fdigits=3):
822 '''
823 Save markers to file.
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 '''
830 return Marker.save_markers(markers, filename, fdigits=fdigits)
833def associate_phases_to_events(markers):
834 '''
835 Reassociate phases to events after import from markers file.
836 '''
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
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)