1# http://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.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 pass
72class Marker(object):
73 '''
74 General purpose marker GUI element and base class for
75 :py:class:`EventMarker` and :py:class:`PhaseMarker`.
77 :param nslc_ids: list of (network, station, location, channel) tuples
78 (may contain wildcards)
79 :param tmin: start time
80 :param tmax: end time
81 :param kind: (optional) integer to distinguish groups of markers
82 (color-coded)
83 '''
85 @staticmethod
86 def save_markers(markers, fn, fdigits=3):
87 '''
88 Static method to write marker objects to file.
90 :param markers: list of :py:class:`Marker` objects
91 :param fn: filename as string
92 :param fdigits: number of decimal digits to use for sub-second time
93 strings (default 3)
94 '''
95 f = open(fn, 'w')
96 f.write('# Snuffler Markers File Version 0.2\n')
97 writer = TableWriter(f)
98 for marker in markers:
99 a = marker.get_attributes(fdigits=fdigits)
100 w = marker.get_attribute_widths(fdigits=fdigits)
101 row = []
102 for x in a:
103 if x is None or x == '':
104 row.append('None')
105 else:
106 row.append(x)
108 writer.writerow(row, w)
110 f.close()
112 @staticmethod
113 def load_markers(fn):
114 '''
115 Static method to load markers from file.
117 :param filename: filename as string
118 :returns: list of :py:class:`Marker`, :py:class:`EventMarker` or
119 :py:class:`PhaseMarker` objects
120 '''
121 markers = []
122 with open(fn, 'r') as f:
123 line = f.readline()
124 if not line.startswith('# Snuffler Markers File Version'):
125 raise MarkerParseError('Not a marker file')
127 elif line.startswith('# Snuffler Markers File Version 0.2'):
128 reader = TableReader(f)
129 while not reader.eof:
130 row = reader.readrow()
131 if not row:
132 continue
133 if row[0] == 'event:':
134 marker = EventMarker.from_attributes(row)
135 elif row[0] == 'phase:':
136 marker = PhaseMarker.from_attributes(row)
137 else:
138 marker = Marker.from_attributes(row)
140 markers.append(marker)
141 else:
142 logger.warning('Unsupported Markers File Version')
144 return markers
146 def __init__(self, nslc_ids, tmin, tmax, kind=0):
147 self.set(nslc_ids, tmin, tmax)
148 self.alerted = False
149 self.selected = False
150 self.kind = kind
151 self.active = False
153 def set(self, nslc_ids, tmin, tmax):
154 '''
155 Set ``nslc_ids``, start time and end time of :py:class:`Marker`.
157 :param nslc_ids: list or set of (network, station, location, channel)
158 tuples
159 :param tmin: start time
160 :param tmax: end time
161 '''
162 self.nslc_ids = nslc_ids
163 self.tmin = util.to_time_float(tmin)
164 self.tmax = util.to_time_float(tmax)
166 def set_kind(self, kind):
167 '''
168 Set kind of :py:class:`Marker`.
170 :param kind: (optional) integer to distinguish groups of markers
171 (color-coded)
172 '''
173 self.kind = kind
175 def get_tmin(self):
176 '''
177 Get *start time* of :py:class:`Marker`.
178 '''
179 return self.tmin
181 def get_tmax(self):
182 '''
183 Get *end time* of :py:class:`Marker`.
184 '''
185 return self.tmax
187 def get_nslc_ids(self):
188 '''
189 Get marker's network-station-location-channel pattern.
191 :returns: list or set of (network, station, location, channel) tuples
193 The network, station, location, or channel strings may contain wildcard
194 expressions.
195 '''
196 return self.nslc_ids
198 def is_alerted(self):
199 return self.alerted
201 def is_selected(self):
202 return self.selected
204 def set_alerted(self, state):
205 self.alerted = state
207 def match_nsl(self, nsl):
208 '''
209 See documentation of :py:func:`pyrocko.util.match_nslc`.
210 '''
211 patterns = ['.'.join(x[:3]) for x in self.nslc_ids]
212 return util.match_nslc(patterns, nsl)
214 def match_nslc(self, nslc):
215 '''
216 See documentation of :py:func:`pyrocko.util.match_nslc`.
217 '''
218 patterns = ['.'.join(x) for x in self.nslc_ids]
219 return util.match_nslc(patterns, nslc)
221 def one_nslc(self):
222 '''
223 If one *nslc_id* defines this marker return this id.
224 If more than one *nslc_id* is defined in the :py:class:`Marker`s
225 *nslc_ids* raise :py:exc:`MarkerOneNSLCRequired`.
226 '''
227 if len(self.nslc_ids) != 1:
228 raise MarkerOneNSLCRequired()
230 return list(self.nslc_ids)[0]
232 def hoover_message(self):
233 return ''
235 def copy(self):
236 '''
237 Get a copy of this marker.
238 '''
239 return copy.deepcopy(self)
241 def __str__(self):
242 traces = ','.join(['.'.join(nslc_id) for nslc_id in self.nslc_ids])
243 st = myctime
244 if self.tmin == self.tmax:
245 return '%s %i %s' % (st(self.tmin), self.kind, traces)
246 else:
247 return '%s %s %g %i %s' % (
248 st(self.tmin), st(self.tmax), self.tmax-self.tmin, self.kind,
249 traces)
251 def get_attributes(self, fdigits=3):
252 traces = ','.join(['.'.join(nslc_id) for nslc_id in self.nslc_ids])
254 def st(t):
255 return util.time_to_str(
256 t, format='%Y-%m-%d %H:%M:%S.'+'%iFRAC' % fdigits)
258 vals = []
259 vals.extend(st(self.tmin).split())
260 if self.tmin != self.tmax:
261 vals.extend(st(self.tmax).split())
262 vals.append(self.tmax-self.tmin)
264 vals.append(self.kind)
265 vals.append(traces)
266 return vals
268 def get_attribute_widths(self, fdigits=3):
269 ws = [10, 9+fdigits]
270 if self.tmin != self.tmax:
271 ws.extend([10, 9+fdigits, 12])
272 ws.extend([2, 15])
273 return ws
275 @staticmethod
276 def parse_attributes(vals):
277 tmin = util.str_to_time(vals[0] + ' ' + vals[1])
278 i = 2
279 tmax = tmin
280 if len(vals) == 7:
281 tmax = util.str_to_time(vals[2] + ' ' + vals[3])
282 i = 5
284 kind = int(vals[i])
285 traces = vals[i+1]
286 if traces == 'None':
287 nslc_ids = []
288 else:
289 nslc_ids = tuple(
290 [tuple(nslc_id.split('.')) for nslc_id in traces.split(',')])
292 return nslc_ids, tmin, tmax, kind
294 @staticmethod
295 def from_attributes(vals):
296 return Marker(*Marker.parse_attributes(vals))
298 def select_color(self, colorlist):
300 def cl(x):
301 return colorlist[(self.kind*3+x) % len(colorlist)]
303 if self.selected:
304 return cl(1)
306 if self.alerted:
307 return cl(1)
309 return cl(2)
311 def draw(
312 self, p, time_projection, y_projection,
313 draw_line=True,
314 draw_triangle=False,
315 **kwargs):
317 from .qt_compat import qc, qg
318 from . import util as gui_util
320 color = self.select_color(g_color_b)
321 pen = qg.QPen(qg.QColor(*color))
322 pen.setWidth(2)
323 p.setPen(pen)
325 umin = time_projection(self.tmin)
326 umax = time_projection(self.tmax)
327 v0, v1 = y_projection.get_out_range()
328 line = qc.QLineF(umin-1, v0, umax+1, v0)
329 p.drawLine(line)
331 if self.selected or self.alerted or not self.nslc_ids:
332 linepen = qg.QPen(pen)
333 if self.selected or self.alerted:
334 linepen.setStyle(qc.Qt.CustomDashLine)
335 pat = [5., 3.]
336 linepen.setDashPattern(pat)
337 if self.alerted and not self.selected:
338 linepen.setColor(qg.QColor(150, 150, 150))
340 s = 9.
341 utriangle = gui_util.make_QPolygonF(
342 [-0.577*s, 0., 0.577*s], [0., 1.*s, 0.])
343 ltriangle = gui_util.make_QPolygonF(
344 [-0.577*s, 0., 0.577*s], [0., -1.*s, 0.])
346 def drawline(t):
347 u = time_projection(t)
348 line = qc.QLineF(u, v0, u, v1)
349 p.drawLine(line)
351 def drawtriangles(t):
352 u = time_projection(t)
353 t = qg.QPolygonF(utriangle)
354 t.translate(u, v0)
355 p.drawConvexPolygon(t)
356 t = qg.QPolygonF(ltriangle)
357 t.translate(u, v1)
358 p.drawConvexPolygon(t)
360 if draw_line or self.selected or self.alerted:
361 p.setPen(linepen)
362 drawline(self.tmin)
363 drawline(self.tmax)
365 if draw_triangle:
366 pen.setStyle(qc.Qt.SolidLine)
367 pen.setJoinStyle(qc.Qt.MiterJoin)
368 pen.setWidth(2)
369 p.setPen(pen)
370 p.setBrush(qg.QColor(*color))
371 drawtriangles(self.tmin)
373 def draw_trace(
374 self, viewer, p, tr, time_projection, track_projection, gain,
375 outline_label=False):
377 from .qt_compat import qc, qg
378 from . import util as gui_util
380 if self.nslc_ids and not self.match_nslc(tr.nslc_id):
381 return
383 color = self.select_color(g_color_b)
384 pen = qg.QPen(qg.QColor(*color))
385 pen.setWidth(2)
386 p.setPen(pen)
387 p.setBrush(qc.Qt.NoBrush)
389 def drawpoint(t, y):
390 u = time_projection(t)
391 v = track_projection(y)
392 rect = qc.QRectF(u-2, v-2, 4, 4)
393 p.drawRect(rect)
395 def drawline(t):
396 u = time_projection(t)
397 v0, v1 = track_projection.get_out_range()
398 line = qc.QLineF(u, v0, u, v1)
399 p.drawLine(line)
401 try:
402 snippet = tr.chop(
403 self.tmin, self.tmax,
404 inplace=False,
405 include_last=True,
406 snap=(math.ceil, math.floor))
408 vdata = track_projection(gain*snippet.get_ydata())
409 udata_min = float(
410 time_projection(snippet.tmin))
411 udata_max = float(
412 time_projection(snippet.tmin+snippet.deltat*(vdata.size-1)))
413 udata = num.linspace(udata_min, udata_max, vdata.size)
414 qpoints = gui_util.make_QPolygonF(udata, vdata)
415 pen.setWidth(1)
416 p.setPen(pen)
417 p.drawPolyline(qpoints)
418 pen.setWidth(2)
419 p.setPen(pen)
420 drawpoint(*tr(self.tmin, clip=True, snap=math.ceil))
421 drawpoint(*tr(self.tmax, clip=True, snap=math.floor))
423 except trace.NoData:
424 pass
426 color = self.select_color(g_color_b)
427 pen = qg.QPen(qg.QColor(*color))
428 pen.setWidth(2)
429 p.setPen(pen)
431 drawline(self.tmin)
432 drawline(self.tmax)
434 label = self.get_label()
435 if label:
436 label_bg = qg.QBrush(qg.QColor(255, 255, 255))
438 u = time_projection(self.tmin)
439 v0, v1 = track_projection.get_out_range()
440 if outline_label:
441 du = -7
442 else:
443 du = -5
444 gui_util.draw_label(
445 p, u+du, v0, label, label_bg, 'TR',
446 outline=outline_label)
448 if self.tmin == self.tmax:
449 try:
450 drawpoint(self.tmin, tr.interpolate(self.tmin))
452 except IndexError:
453 pass
455 def get_label(self):
456 return None
458 def convert_to_phase_marker(
459 self,
460 event=None,
461 phasename=None,
462 polarity=None,
463 automatic=None,
464 incidence_angle=None,
465 takeoff_angle=None):
467 if isinstance(self, PhaseMarker):
468 return
470 self.__class__ = PhaseMarker
471 self._event = event
472 self._phasename = phasename
473 self._polarity = polarity
474 self._automatic = automatic
475 self._incidence_angle = incidence_angle
476 self._takeoff_angle = takeoff_angle
477 if self._event:
478 self._event_hash = event.get_hash()
479 self._event_time = event.time
480 else:
481 self._event_hash = None
482 self._event_time = None
483 self.active = False
485 def convert_to_event_marker(self, lat=0., lon=0.):
486 if isinstance(self, EventMarker):
487 return
489 if isinstance(self, PhaseMarker):
490 self.convert_to_marker()
492 self.__class__ = EventMarker
493 self._event = model.Event(lat, lon, time=self.tmin, name='Event')
494 self._event_hash = self._event.get_hash()
495 self.active = False
496 self.tmax = self.tmin
497 self.nslc_ids = []
500class EventMarker(Marker):
501 '''
502 GUI element representing a seismological event.
504 :param event: A :py:class:`pyrocko.model.Event` object containing meta
505 information of a seismological event
506 :param kind: (optional) integer to distinguish groups of markers
507 :param event_hash: (optional) hash code of event (see:
508 :py:meth:`pyrocko.model.Event.get_hash`)
509 '''
511 def __init__(self, event, kind=0, event_hash=None):
512 Marker.__init__(self, [], event.time, event.time, kind)
513 self._event = event
514 self.active = False
515 self._event_hash = event_hash
517 def get_event_hash(self):
518 if self._event_hash is not None:
519 return self._event_hash
520 else:
521 return self._event.get_hash()
523 def label(self):
524 t = []
525 mag = self._event.magnitude
526 if mag is not None:
527 t.append('M%3.1f' % mag)
529 reg = self._event.region
530 if reg is not None:
531 t.append(reg)
533 nam = self._event.name
534 if nam is not None:
535 t.append(nam)
537 s = ' '.join(t)
538 if not s:
539 s = '(Event)'
540 return s
542 def draw(self, p, time_projection, y_projection, with_label=False):
543 Marker.draw(
544 self, p, time_projection, y_projection,
545 draw_line=False,
546 draw_triangle=True)
548 if with_label:
549 self.draw_label(p, time_projection, y_projection)
551 def draw_label(self, p, time_projection, y_projection):
552 from .qt_compat import qg
553 from . import util as gui_util
555 u = time_projection(self.tmin)
556 v0, v1 = y_projection.get_out_range()
557 label_bg = qg.QBrush(qg.QColor(255, 255, 255))
558 gui_util.draw_label(
559 p, u, v0-10., self.label(), label_bg, 'CB',
560 outline=self.active)
562 def get_event(self):
563 '''
564 Return an instance of the :py:class:`pyrocko.model.Event` associated
565 to this :py:class:`EventMarker`
566 '''
567 return self._event
569 def draw_trace(self, viewer, p, tr, time_projection, track_projection,
570 gain):
571 pass
573 def hoover_message(self):
574 ev = self.get_event()
575 evs = []
576 for k in 'magnitude lat lon depth name region catalog'.split():
577 if ev.__dict__[k] is not None and ev.__dict__[k] != '':
578 if k == 'depth':
579 sv = '%g km' % (ev.depth * 0.001)
580 else:
581 sv = '%s' % ev.__dict__[k]
582 evs.append('%s = %s' % (k, sv))
584 return ', '.join(evs)
586 def get_attributes(self, fdigits=3):
587 attributes = ['event:']
588 attributes.extend(Marker.get_attributes(self, fdigits=fdigits))
589 del attributes[-1]
590 e = self._event
591 attributes.extend([
592 e.get_hash(), e.lat, e.lon, e.depth, e.magnitude, e.catalog,
593 e.name, e.region])
595 return attributes
597 def get_attribute_widths(self, fdigits=3):
598 ws = [6]
599 ws.extend(Marker.get_attribute_widths(self, fdigits=fdigits))
600 del ws[-1]
601 ws.extend([14, 12, 12, 12, 4, 5, 0, 0])
602 return ws
604 @staticmethod
605 def from_attributes(vals):
607 nslc_ids, tmin, tmax, kind = Marker.parse_attributes(
608 vals[1:] + ['None'])
609 lat, lon, depth, magnitude = [
610 str_to_float_or_none(x) for x in vals[5:9]]
611 catalog, name, region = [
612 str_to_str_or_none(x) for x in vals[9:]]
613 e = model.Event(
614 lat, lon, time=tmin, name=name, depth=depth, magnitude=magnitude,
615 region=region, catalog=catalog)
616 marker = EventMarker(
617 e, kind, event_hash=str_to_str_or_none(vals[4]))
618 return marker
621class PhaseMarker(Marker):
622 '''
623 A PhaseMarker is a GUI-element representing a seismological phase arrival
625 :param nslc_ids: list of (network, station, location, channel) tuples (may
626 contain wildcards)
627 :param tmin: start time
628 :param tmax: end time
629 :param kind: (optional) integer to distinguish groups of markers
630 (color-coded)
631 :param event: a :py:class:`pyrocko.model.Event` object containing meta
632 information of a seismological event
633 :param event_hash: (optional) hash code of event (see:
634 :py:meth:`pyrocko.model.Event.get_hash`)
635 :param event_time: (optional) time of the associated event
636 :param phasename: (optional) name of the phase associated with the marker
637 :param polarity: (optional) polarity of arriving phase
638 :param automatic: (optional)
639 :param incident_angle: (optional) incident angle of phase
640 :param takeoff_angle: (optional) take off angle of phase
641 '''
642 def __init__(
643 self, nslc_ids, tmin, tmax,
644 kind=0,
645 event=None,
646 event_hash=None,
647 event_time=None,
648 phasename=None,
649 polarity=None,
650 automatic=None,
651 incidence_angle=None,
652 takeoff_angle=None):
654 Marker.__init__(self, nslc_ids, tmin, tmax, kind)
655 self._event = event
656 self._event_hash = event_hash
657 self._event_time = event_time
658 self._phasename = phasename
659 self._automatic = automatic
660 self._incidence_angle = incidence_angle
661 self._takeoff_angle = takeoff_angle
663 self.set_polarity(polarity)
665 def draw_trace(self, viewer, p, tr, time_projection, track_projection,
666 gain):
668 Marker.draw_trace(
669 self, viewer, p, tr, time_projection, track_projection, gain,
670 outline_label=(
671 self._event is not None and
672 self._event == viewer.get_active_event()))
674 def get_label(self):
675 t = []
676 if self._phasename is not None:
677 t.append(self._phasename)
678 if self._polarity is not None:
679 t.append(self.get_polarity_symbol())
681 if self._automatic:
682 t.append('@')
684 return ''.join(t)
686 def get_event(self):
687 '''
688 Return an instance of the :py:class:`pyrocko.model.Event` associated
689 to this :py:class:`EventMarker`
690 '''
691 return self._event
693 def get_event_hash(self):
694 if self._event_hash is not None:
695 return self._event_hash
696 else:
697 if self._event is None:
698 return None
699 else:
700 return self._event.get_hash()
702 def get_event_time(self):
703 if self._event is not None:
704 return self._event.time
705 else:
706 return self._event_time
708 def set_event_hash(self, event_hash):
709 self._event_hash = event_hash
711 def set_event(self, event):
712 self._event = event
713 if event is not None:
714 self.set_event_hash(event.get_hash())
716 def get_phasename(self):
717 return self._phasename
719 def set_phasename(self, phasename):
720 self._phasename = phasename
722 def set_polarity(self, polarity):
723 if polarity not in [1, -1, 0, None]:
724 raise ValueError('polarity has to be 1, -1, 0 or None')
725 self._polarity = polarity
727 def get_polarity_symbol(self):
728 return polarity_symbols.get(self._polarity, '')
730 def get_polarity(self):
731 return self._polarity
733 def convert_to_marker(self):
734 del self._event
735 del self._event_hash
736 del self._phasename
737 del self._polarity
738 del self._automatic
739 del self._incidence_angle
740 del self._takeoff_angle
741 self.active = False
742 self.__class__ = Marker
744 def hoover_message(self):
745 toks = []
746 for k in 'incidence_angle takeoff_angle polarity'.split():
747 v = getattr(self, '_' + k)
748 if v is not None:
749 toks.append('%s = %s' % (k, v))
751 return ', '.join(toks)
753 def get_attributes(self, fdigits=3):
754 attributes = ['phase:']
755 attributes.extend(Marker.get_attributes(self, fdigits=fdigits))
757 et = None, None
758 if self._event:
759 et = self._st(self._event.time, fdigits).split()
760 elif self._event_time:
761 et = self._st(self._event_time, fdigits).split()
763 attributes.extend([
764 self.get_event_hash(), et[0], et[1], self._phasename,
765 self._polarity, self._automatic])
767 return attributes
769 def _st(self, t, fdigits):
770 return util.time_to_str(
771 t, format='%Y-%m-%d %H:%M:%S.'+'%iFRAC' % fdigits)
773 def get_attribute_widths(self, fdigits=3):
774 ws = [6]
775 ws.extend(Marker.get_attribute_widths(self, fdigits=fdigits))
776 ws.extend([14, 12, 12, 8, 4, 5])
777 return ws
779 @staticmethod
780 def from_attributes(vals):
781 if len(vals) == 14:
782 nbasicvals = 7
783 else:
784 nbasicvals = 4
785 nslc_ids, tmin, tmax, kind = Marker.parse_attributes(
786 vals[1:1+nbasicvals])
788 i = 8
789 if len(vals) == 14:
790 i = 11
792 event_hash = str_to_str_or_none(vals[i-3])
793 event_sdate = str_to_str_or_none(vals[i-2])
794 event_stime = str_to_str_or_none(vals[i-1])
796 if event_sdate is not None and event_stime is not None:
797 event_time = util.str_to_time(event_sdate + ' ' + event_stime)
798 else:
799 event_time = None
801 phasename = str_to_str_or_none(vals[i])
802 polarity = str_to_int_or_none(vals[i+1])
803 automatic = str_to_bool(vals[i+2])
804 marker = PhaseMarker(nslc_ids, tmin, tmax, kind, event=None,
805 event_hash=event_hash, event_time=event_time,
806 phasename=phasename, polarity=polarity,
807 automatic=automatic)
808 return marker
811def load_markers(filename):
812 '''
813 Load markers from file.
815 :param filename: filename as string
816 :returns: list of :py:class:`Marker` Objects
817 '''
819 return Marker.load_markers(filename)
822def save_markers(markers, filename, fdigits=3):
823 '''
824 Save markers to file.
826 :param markers: list of :py:class:`Marker` Objects
827 :param filename: filename as string
828 :param fdigits: number of decimal digits to use for sub-second time strings
829 '''
831 return Marker.save_markers(markers, filename, fdigits=fdigits)
834def associate_phases_to_events(markers):
835 '''
836 Reassociate phases to events after import from markers file.
837 '''
839 hash_to_events = {}
840 time_to_events = {}
841 for marker in markers:
842 if isinstance(marker, EventMarker):
843 ev = marker.get_event()
844 hash_to_events[marker.get_event_hash()] = ev
845 time_to_events[ev.time] = ev
847 for marker in markers:
848 if isinstance(marker, PhaseMarker):
849 h = marker.get_event_hash()
850 t = marker.get_event_time()
851 if marker.get_event() is None:
852 if h is not None and h in hash_to_events:
853 marker.set_event(hash_to_events[h])
854 marker.set_event_hash(None)
855 elif t is not None and t in time_to_events:
856 marker.set_event(time_to_events[t])
857 marker.set_event_hash(None)