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',
61 'butter1', 'butter2', 'butter3',
62 'aluminium3', 'aluminium4', 'aluminium5')]
65class MarkerParseError(Exception):
66 pass
69class MarkerOneNSLCRequired(Exception):
70 pass
73class Marker(object):
74 '''
75 General purpose marker GUI element and base class for
76 :py:class:`EventMarker` and :py:class:`PhaseMarker`.
78 :param nslc_ids: list of (network, station, location, channel) tuples
79 (may contain wildcards)
80 :param tmin: start time
81 :param tmax: end time
82 :param kind: (optional) integer to distinguish groups of markers
83 (color-coded)
84 '''
86 @staticmethod
87 def save_markers(markers, fn, fdigits=3):
88 '''
89 Static method to write marker objects to file.
91 :param markers: list of :py:class:`Marker` objects
92 :param fn: filename as string
93 :param fdigits: number of decimal digits to use for sub-second time
94 strings (default 3)
95 '''
96 f = open(fn, 'w')
97 f.write('# Snuffler Markers File Version 0.2\n')
98 writer = TableWriter(f)
99 for marker in markers:
100 a = marker.get_attributes(fdigits=fdigits)
101 w = marker.get_attribute_widths(fdigits=fdigits)
102 row = []
103 for x in a:
104 if x is None or x == '':
105 row.append('None')
106 else:
107 row.append(x)
109 writer.writerow(row, w)
111 f.close()
113 @staticmethod
114 def load_markers(fn):
115 '''
116 Static method to load markers from file.
118 :param filename: filename as string
119 :returns: list of :py:class:`Marker`, :py:class:`EventMarker` or
120 :py:class:`PhaseMarker` objects
121 '''
122 markers = []
123 with open(fn, 'r') as f:
124 line = f.readline()
125 if not line.startswith('# Snuffler Markers File Version'):
126 raise MarkerParseError('Not a marker file')
128 elif line.startswith('# Snuffler Markers File Version 0.2'):
129 reader = TableReader(f)
130 while not reader.eof:
131 row = reader.readrow()
132 if not row:
133 continue
134 if row[0] == 'event:':
135 marker = EventMarker.from_attributes(row)
136 elif row[0] == 'phase:':
137 marker = PhaseMarker.from_attributes(row)
138 else:
139 marker = Marker.from_attributes(row)
141 markers.append(marker)
142 else:
143 logger.warning('Unsupported Markers File Version')
145 return markers
147 def __init__(self, nslc_ids, tmin, tmax, kind=0):
148 self.set(nslc_ids, tmin, tmax)
149 self.alerted = False
150 self.selected = False
151 self.kind = kind
152 self.active = False
154 def set(self, nslc_ids, tmin, tmax):
155 '''
156 Set ``nslc_ids``, start time and end time of :py:class:`Marker`.
158 :param nslc_ids: list or set of (network, station, location, channel)
159 tuples
160 :param tmin: start time
161 :param tmax: end time
162 '''
163 self.nslc_ids = nslc_ids
164 self.tmin = util.to_time_float(tmin)
165 self.tmax = util.to_time_float(tmax)
167 def set_kind(self, kind):
168 '''
169 Set kind of :py:class:`Marker`.
171 :param kind: (optional) integer to distinguish groups of markers
172 (color-coded)
173 '''
174 self.kind = kind
176 def get_tmin(self):
177 '''
178 Get *start time* of :py:class:`Marker`.
179 '''
180 return self.tmin
182 def get_tmax(self):
183 '''
184 Get *end time* of :py:class:`Marker`.
185 '''
186 return self.tmax
188 def get_nslc_ids(self):
189 '''
190 Get marker's network-station-location-channel pattern.
192 :returns: list or set of (network, station, location, channel) tuples
194 The network, station, location, or channel strings may contain wildcard
195 expressions.
196 '''
197 return self.nslc_ids
199 def is_alerted(self):
200 return self.alerted
202 def is_selected(self):
203 return self.selected
205 def set_alerted(self, state):
206 self.alerted = state
208 def match_nsl(self, nsl):
209 '''
210 See documentation of :py:func:`pyrocko.util.match_nslc`.
211 '''
212 patterns = ['.'.join(x[:3]) for x in self.nslc_ids]
213 return util.match_nslc(patterns, nsl)
215 def match_nslc(self, nslc):
216 '''
217 See documentation of :py:func:`pyrocko.util.match_nslc`.
218 '''
219 patterns = ['.'.join(x) for x in self.nslc_ids]
220 return util.match_nslc(patterns, nslc)
222 def one_nslc(self):
223 '''
224 If one *nslc_id* defines this marker return this id.
225 If more than one *nslc_id* is defined in the :py:class:`Marker`s
226 *nslc_ids* raise :py:exc:`MarkerOneNSLCRequired`.
227 '''
228 if len(self.nslc_ids) != 1:
229 raise MarkerOneNSLCRequired()
231 return list(self.nslc_ids)[0]
233 def hoover_message(self):
234 return ''
236 def copy(self):
237 '''
238 Get a copy of this marker.
239 '''
240 return copy.deepcopy(self)
242 def __str__(self):
243 traces = ','.join(['.'.join(nslc_id) for nslc_id in self.nslc_ids])
244 st = myctime
245 if self.tmin == self.tmax:
246 return '%s %i %s' % (st(self.tmin), self.kind, traces)
247 else:
248 return '%s %s %g %i %s' % (
249 st(self.tmin), st(self.tmax), self.tmax-self.tmin, self.kind,
250 traces)
252 def get_attributes(self, fdigits=3):
253 traces = ','.join(['.'.join(nslc_id) for nslc_id in self.nslc_ids])
255 def st(t):
256 return util.time_to_str(
257 t, format='%Y-%m-%d %H:%M:%S.'+'%iFRAC' % fdigits)
259 vals = []
260 vals.extend(st(self.tmin).split())
261 if self.tmin != self.tmax:
262 vals.extend(st(self.tmax).split())
263 vals.append(self.tmax-self.tmin)
265 vals.append(self.kind)
266 vals.append(traces)
267 return vals
269 def get_attribute_widths(self, fdigits=3):
270 ws = [10, 9+fdigits]
271 if self.tmin != self.tmax:
272 ws.extend([10, 9+fdigits, 12])
273 ws.extend([2, 15])
274 return ws
276 @staticmethod
277 def parse_attributes(vals):
278 tmin = util.str_to_time(vals[0] + ' ' + vals[1])
279 i = 2
280 tmax = tmin
281 if len(vals) == 7:
282 tmax = util.str_to_time(vals[2] + ' ' + vals[3])
283 i = 5
285 kind = int(vals[i])
286 traces = vals[i+1]
287 if traces == 'None':
288 nslc_ids = []
289 else:
290 nslc_ids = tuple(
291 [tuple(nslc_id.split('.')) for nslc_id in traces.split(',')])
293 return nslc_ids, tmin, tmax, kind
295 @staticmethod
296 def from_attributes(vals):
297 return Marker(*Marker.parse_attributes(vals))
299 def select_color(self, colorlist):
301 def cl(x):
302 return colorlist[(self.kind*3+x) % len(colorlist)]
304 if self.selected:
305 return cl(1)
307 if self.alerted:
308 return cl(1)
310 return cl(2)
312 def draw(
313 self, p, time_projection, y_projection,
314 draw_line=True,
315 draw_triangle=False,
316 **kwargs):
318 from .qt_compat import qc, qg
319 from . import util as gui_util
321 color = self.select_color(g_color_b)
322 pen = qg.QPen(qg.QColor(*color))
323 pen.setWidth(2)
324 p.setPen(pen)
326 umin = time_projection(self.tmin)
327 umax = time_projection(self.tmax)
328 v0, v1 = y_projection.get_out_range()
329 line = qc.QLineF(umin-1, v0, umax+1, v0)
330 p.drawLine(line)
332 if self.selected or self.alerted or not self.nslc_ids:
333 linepen = qg.QPen(pen)
334 if self.selected or self.alerted:
335 linepen.setStyle(qc.Qt.CustomDashLine)
336 pat = [5., 3.]
337 linepen.setDashPattern(pat)
338 if self.alerted and not self.selected:
339 linepen.setColor(qg.QColor(150, 150, 150))
341 s = 9.
342 utriangle = gui_util.make_QPolygonF(
343 [-0.577*s, 0., 0.577*s], [0., 1.*s, 0.])
344 ltriangle = gui_util.make_QPolygonF(
345 [-0.577*s, 0., 0.577*s], [0., -1.*s, 0.])
347 def drawline(t):
348 u = time_projection(t)
349 line = qc.QLineF(u, v0, u, v1)
350 p.drawLine(line)
352 def drawtriangles(t):
353 u = time_projection(t)
354 t = qg.QPolygonF(utriangle)
355 t.translate(u, v0)
356 p.drawConvexPolygon(t)
357 t = qg.QPolygonF(ltriangle)
358 t.translate(u, v1)
359 p.drawConvexPolygon(t)
361 if draw_line or self.selected or self.alerted:
362 p.setPen(linepen)
363 drawline(self.tmin)
364 drawline(self.tmax)
366 if draw_triangle:
367 pen.setStyle(qc.Qt.SolidLine)
368 pen.setJoinStyle(qc.Qt.MiterJoin)
369 pen.setWidth(2)
370 p.setPen(pen)
371 p.setBrush(qg.QColor(*color))
372 drawtriangles(self.tmin)
374 def draw_trace(
375 self, viewer, p, tr, time_projection, track_projection, gain,
376 outline_label=False):
378 from .qt_compat import qc, qg
379 from . import util as gui_util
381 if self.nslc_ids and not self.match_nslc(tr.nslc_id):
382 return
384 color = self.select_color(g_color_b)
385 pen = qg.QPen(qg.QColor(*color))
386 pen.setWidth(2)
387 p.setPen(pen)
388 p.setBrush(qc.Qt.NoBrush)
390 def drawpoint(t, y):
391 u = time_projection(t)
392 v = track_projection(y)
393 rect = qc.QRectF(u-2, v-2, 4, 4)
394 p.drawRect(rect)
396 def drawline(t):
397 u = time_projection(t)
398 v0, v1 = track_projection.get_out_range()
399 line = qc.QLineF(u, v0, u, v1)
400 p.drawLine(line)
402 try:
403 snippet = tr.chop(
404 self.tmin, self.tmax,
405 inplace=False,
406 include_last=True,
407 snap=(math.ceil, math.floor))
409 vdata = track_projection(gain*snippet.get_ydata())
410 udata_min = float(
411 time_projection(snippet.tmin))
412 udata_max = float(
413 time_projection(snippet.tmin+snippet.deltat*(vdata.size-1)))
414 udata = num.linspace(udata_min, udata_max, vdata.size)
415 qpoints = gui_util.make_QPolygonF(udata, vdata)
416 pen.setWidth(1)
417 p.setPen(pen)
418 p.drawPolyline(qpoints)
419 pen.setWidth(2)
420 p.setPen(pen)
421 drawpoint(*tr(self.tmin, clip=True, snap=math.ceil))
422 drawpoint(*tr(self.tmax, clip=True, snap=math.floor))
424 except trace.NoData:
425 pass
427 color = self.select_color(g_color_b)
428 pen = qg.QPen(qg.QColor(*color))
429 pen.setWidth(2)
430 p.setPen(pen)
432 drawline(self.tmin)
433 drawline(self.tmax)
435 label = self.get_label()
436 if label:
437 label_bg = qg.QBrush(qg.QColor(255, 255, 255))
439 u = time_projection(self.tmin)
440 v0, v1 = track_projection.get_out_range()
441 if outline_label:
442 du = -7
443 else:
444 du = -5
445 gui_util.draw_label(
446 p, u+du, v0, label, label_bg, 'TR',
447 outline=outline_label)
449 if self.tmin == self.tmax:
450 try:
451 drawpoint(self.tmin, tr.interpolate(self.tmin))
453 except IndexError:
454 pass
456 def get_label(self):
457 return None
459 def convert_to_phase_marker(
460 self,
461 event=None,
462 phasename=None,
463 polarity=None,
464 automatic=None,
465 incidence_angle=None,
466 takeoff_angle=None):
468 if isinstance(self, PhaseMarker):
469 return
471 self.__class__ = PhaseMarker
472 self._event = event
473 self._phasename = phasename
474 self._polarity = polarity
475 self._automatic = automatic
476 self._incidence_angle = incidence_angle
477 self._takeoff_angle = takeoff_angle
478 if self._event:
479 self._event_hash = event.get_hash()
480 self._event_time = event.time
481 else:
482 self._event_hash = None
483 self._event_time = None
484 self.active = False
486 def convert_to_event_marker(self, lat=0., lon=0.):
487 if isinstance(self, EventMarker):
488 return
490 if isinstance(self, PhaseMarker):
491 self.convert_to_marker()
493 self.__class__ = EventMarker
494 self._event = model.Event(lat, lon, time=self.tmin, name='Event')
495 self._event_hash = self._event.get_hash()
496 self.active = False
497 self.tmax = self.tmin
498 self.nslc_ids = []
501class EventMarker(Marker):
502 '''
503 GUI element representing a seismological event.
505 :param event: A :py:class:`pyrocko.model.Event` object containing meta
506 information of a seismological event
507 :param kind: (optional) integer to distinguish groups of markers
508 :param event_hash: (optional) hash code of event (see:
509 :py:meth:`pyrocko.model.Event.get_hash`)
510 '''
512 def __init__(self, event, kind=0, event_hash=None):
513 Marker.__init__(self, [], event.time, event.time, kind)
514 self._event = event
515 self.active = False
516 self._event_hash = event_hash
518 def get_event_hash(self):
519 if self._event_hash is not None:
520 return self._event_hash
521 else:
522 return self._event.get_hash()
524 def label(self):
525 t = []
526 mag = self._event.magnitude
527 if mag is not None:
528 t.append('M%3.1f' % mag)
530 reg = self._event.region
531 if reg is not None:
532 t.append(reg)
534 nam = self._event.name
535 if nam is not None:
536 t.append(nam)
538 s = ' '.join(t)
539 if not s:
540 s = '(Event)'
541 return s
543 def draw(self, p, time_projection, y_projection, with_label=False):
544 Marker.draw(
545 self, p, time_projection, y_projection,
546 draw_line=False,
547 draw_triangle=True)
549 if with_label:
550 self.draw_label(p, time_projection, y_projection)
552 def draw_label(self, p, time_projection, y_projection):
553 from .qt_compat import qg
554 from . import util as gui_util
556 u = time_projection(self.tmin)
557 v0, v1 = y_projection.get_out_range()
558 label_bg = qg.QBrush(qg.QColor(255, 255, 255))
559 gui_util.draw_label(
560 p, u, v0-10., self.label(), label_bg, 'CB',
561 outline=self.active)
563 def get_event(self):
564 '''
565 Return an instance of the :py:class:`pyrocko.model.Event` associated
566 to this :py:class:`EventMarker`
567 '''
568 return self._event
570 def draw_trace(self, viewer, p, tr, time_projection, track_projection,
571 gain):
572 pass
574 def hoover_message(self):
575 ev = self.get_event()
576 evs = []
577 for k in 'magnitude lat lon depth name region catalog'.split():
578 if ev.__dict__[k] is not None and ev.__dict__[k] != '':
579 if k == 'depth':
580 sv = '%g km' % (ev.depth * 0.001)
581 else:
582 sv = '%s' % ev.__dict__[k]
583 evs.append('%s = %s' % (k, sv))
585 return ', '.join(evs)
587 def get_attributes(self, fdigits=3):
588 attributes = ['event:']
589 attributes.extend(Marker.get_attributes(self, fdigits=fdigits))
590 del attributes[-1]
591 e = self._event
592 attributes.extend([
593 e.get_hash(), e.lat, e.lon, e.depth, e.magnitude, e.catalog,
594 e.name, e.region])
596 return attributes
598 def get_attribute_widths(self, fdigits=3):
599 ws = [6]
600 ws.extend(Marker.get_attribute_widths(self, fdigits=fdigits))
601 del ws[-1]
602 ws.extend([14, 12, 12, 12, 4, 5, 0, 0])
603 return ws
605 @staticmethod
606 def from_attributes(vals):
608 nslc_ids, tmin, tmax, kind = Marker.parse_attributes(
609 vals[1:] + ['None'])
610 lat, lon, depth, magnitude = [
611 str_to_float_or_none(x) for x in vals[5:9]]
612 catalog, name, region = [
613 str_to_str_or_none(x) for x in vals[9:]]
614 e = model.Event(
615 lat, lon, time=tmin, name=name, depth=depth, magnitude=magnitude,
616 region=region, catalog=catalog)
617 marker = EventMarker(
618 e, kind, event_hash=str_to_str_or_none(vals[4]))
619 return marker
622class PhaseMarker(Marker):
623 '''
624 A PhaseMarker is a GUI-element representing a seismological phase arrival
626 :param nslc_ids: list of (network, station, location, channel) tuples (may
627 contain wildcards)
628 :param tmin: start time
629 :param tmax: end time
630 :param kind: (optional) integer to distinguish groups of markers
631 (color-coded)
632 :param event: a :py:class:`pyrocko.model.Event` object containing meta
633 information of a seismological event
634 :param event_hash: (optional) hash code of event (see:
635 :py:meth:`pyrocko.model.Event.get_hash`)
636 :param event_time: (optional) time of the associated event
637 :param phasename: (optional) name of the phase associated with the marker
638 :param polarity: (optional) polarity of arriving phase
639 :param automatic: (optional)
640 :param incident_angle: (optional) incident angle of phase
641 :param takeoff_angle: (optional) take off angle of phase
642 '''
643 def __init__(
644 self, nslc_ids, tmin, tmax,
645 kind=0,
646 event=None,
647 event_hash=None,
648 event_time=None,
649 phasename=None,
650 polarity=None,
651 automatic=None,
652 incidence_angle=None,
653 takeoff_angle=None):
655 Marker.__init__(self, nslc_ids, tmin, tmax, kind)
656 self._event = event
657 self._event_hash = event_hash
658 self._event_time = event_time
659 self._phasename = phasename
660 self._automatic = automatic
661 self._incidence_angle = incidence_angle
662 self._takeoff_angle = takeoff_angle
664 self.set_polarity(polarity)
666 def draw_trace(self, viewer, p, tr, time_projection, track_projection,
667 gain):
669 Marker.draw_trace(
670 self, viewer, p, tr, time_projection, track_projection, gain,
671 outline_label=(
672 self._event is not None and
673 self._event == viewer.get_active_event()))
675 def get_label(self):
676 t = []
677 if self._phasename is not None:
678 t.append(self._phasename)
679 if self._polarity is not None:
680 t.append(self.get_polarity_symbol())
682 if self._automatic:
683 t.append('@')
685 return ''.join(t)
687 def get_event(self):
688 '''
689 Return an instance of the :py:class:`pyrocko.model.Event` associated
690 to this :py:class:`EventMarker`
691 '''
692 return self._event
694 def get_event_hash(self):
695 if self._event_hash is not None:
696 return self._event_hash
697 else:
698 if self._event is None:
699 return None
700 else:
701 return self._event.get_hash()
703 def get_event_time(self):
704 if self._event is not None:
705 return self._event.time
706 else:
707 return self._event_time
709 def set_event_hash(self, event_hash):
710 self._event_hash = event_hash
712 def set_event(self, event):
713 self._event = event
714 if event is not None:
715 self.set_event_hash(event.get_hash())
717 def get_phasename(self):
718 return self._phasename
720 def set_phasename(self, phasename):
721 self._phasename = phasename
723 def set_polarity(self, polarity):
724 if polarity not in [1, -1, 0, None]:
725 raise ValueError('polarity has to be 1, -1, 0 or None')
726 self._polarity = polarity
728 def get_polarity_symbol(self):
729 return polarity_symbols.get(self._polarity, '')
731 def get_polarity(self):
732 return self._polarity
734 def convert_to_marker(self):
735 del self._event
736 del self._event_hash
737 del self._phasename
738 del self._polarity
739 del self._automatic
740 del self._incidence_angle
741 del self._takeoff_angle
742 self.active = False
743 self.__class__ = Marker
745 def hoover_message(self):
746 toks = []
747 for k in 'incidence_angle takeoff_angle polarity'.split():
748 v = getattr(self, '_' + k)
749 if v is not None:
750 toks.append('%s = %s' % (k, v))
752 return ', '.join(toks)
754 def get_attributes(self, fdigits=3):
755 attributes = ['phase:']
756 attributes.extend(Marker.get_attributes(self, fdigits=fdigits))
758 et = None, None
759 if self._event:
760 et = self._st(self._event.time, fdigits).split()
761 elif self._event_time:
762 et = self._st(self._event_time, fdigits).split()
764 attributes.extend([
765 self.get_event_hash(), et[0], et[1], self._phasename,
766 self._polarity, self._automatic])
768 return attributes
770 def _st(self, t, fdigits):
771 return util.time_to_str(
772 t, format='%Y-%m-%d %H:%M:%S.'+'%iFRAC' % fdigits)
774 def get_attribute_widths(self, fdigits=3):
775 ws = [6]
776 ws.extend(Marker.get_attribute_widths(self, fdigits=fdigits))
777 ws.extend([14, 12, 12, 8, 4, 5])
778 return ws
780 @staticmethod
781 def from_attributes(vals):
782 if len(vals) == 14:
783 nbasicvals = 7
784 else:
785 nbasicvals = 4
786 nslc_ids, tmin, tmax, kind = Marker.parse_attributes(
787 vals[1:1+nbasicvals])
789 i = 8
790 if len(vals) == 14:
791 i = 11
793 event_hash = str_to_str_or_none(vals[i-3])
794 event_sdate = str_to_str_or_none(vals[i-2])
795 event_stime = str_to_str_or_none(vals[i-1])
797 if event_sdate is not None and event_stime is not None:
798 event_time = util.str_to_time(event_sdate + ' ' + event_stime)
799 else:
800 event_time = None
802 phasename = str_to_str_or_none(vals[i])
803 polarity = str_to_int_or_none(vals[i+1])
804 automatic = str_to_bool(vals[i+2])
805 marker = PhaseMarker(nslc_ids, tmin, tmax, kind, event=None,
806 event_hash=event_hash, event_time=event_time,
807 phasename=phasename, polarity=polarity,
808 automatic=automatic)
809 return marker
812def load_markers(filename):
813 '''
814 Load markers from file.
816 :param filename: filename as string
817 :returns: list of :py:class:`Marker` Objects
818 '''
820 return Marker.load_markers(filename)
823def save_markers(markers, filename, fdigits=3):
824 '''
825 Save markers to file.
827 :param markers: list of :py:class:`Marker` Objects
828 :param filename: filename as string
829 :param fdigits: number of decimal digits to use for sub-second time strings
830 '''
832 return Marker.save_markers(markers, filename, fdigits=fdigits)
835def associate_phases_to_events(markers):
836 '''
837 Reassociate phases to events after import from markers file.
838 '''
840 hash_to_events = {}
841 time_to_events = {}
842 for marker in markers:
843 if isinstance(marker, EventMarker):
844 ev = marker.get_event()
845 hash_to_events[marker.get_event_hash()] = ev
846 time_to_events[ev.time] = ev
848 for marker in markers:
849 if isinstance(marker, PhaseMarker):
850 h = marker.get_event_hash()
851 t = marker.get_event_time()
852 if marker.get_event() is None:
853 if h is not None and h in hash_to_events:
854 marker.set_event(hash_to_events[h])
855 marker.set_event_hash(None)
856 elif t is not None and t in time_to_events:
857 marker.set_event(time_to_events[t])
858 marker.set_event_hash(None)