├── LICENSE.txt
├── README.md
├── img
├── Multichannel.png
├── Singlechannel.png
└── plot_line.png
├── seedlink_plotter
├── __init__.py
├── favicon.gif
└── seedlink_plotter.py
└── setup.py
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Seedlink-Plotter
2 |
3 | A python script to plot real time seismic data from a seedlink server in drum style or line style
4 |
5 | This version works with at least stable ObsPy version 1.3.0
6 |
7 | On some linux box, the time zone must be set to UTC and not GMT
8 |
9 | ### Installation
10 |
11 | pip install https://github.com/bonaime/seedlink_plotter/archive/master.zip
12 |
13 | ### Upgrade
14 |
15 | pip install --upgrade https://github.com/bonaime/seedlink_plotter/archive/master.zip
16 |
17 | ### Usage examples
18 |
19 | Drum plots (with longer time range) and with events greater than 5.5:
20 |
21 | seedlink-plotter -s "G_FDFM:00BHZ" --x_position 200 --y_position 50 --x_size 800 --y_size 600 -b 24h --scale 20000 --seedlink_server "rtserver.ipgp.fr:18000" --x_scale 60m --events 5.5
22 |
23 | 
24 |
25 |
26 | Line plot with single station (with shorter time range):
27 |
28 | seedlink-plotter -s "G_IVI:00BHZ" -b 10m --seedlink_server "rtserver.ipgp.fr:18000" --line_plot
29 |
30 | 
31 |
32 | Line plots with multiple stations (with shorter time range):
33 |
34 | seedlink-plotter -s "G_FDFM:00BHZ,G_SSB:00BHZ" --x_position 200 --y_position 50 --x_size 800 --y_size 600 -b 30m --seedlink_server "rtserver.ipgp.fr:18000" --update_time 2s
35 | seedlink-plotter -s "G_FDFM:00BHZ 00BHN 00BHE" --x_position 200 --y_position 50 --x_size 800 --y_size 600 -b 30m --seedlink_server "rtserver.ipgp.fr:18000" --update_time 2s
36 |
37 | 
38 |
39 | ### Keyboard Controls
40 |
41 | Keyboard controls only work without option `--without-decoration`!
42 |
43 | - `f`: toggle fullscreen
44 | - `` or `q`: close window
45 |
46 | ### Dependencies
47 | - Python 3.6
48 | - ObsPy 1.3.0
49 | - matplolib (>= 1.3.0)
50 | - scipy
51 | - numpy
52 |
--------------------------------------------------------------------------------
/img/Multichannel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbonaime/seedlink_plotter/300ae9057e8bca38a9246acc666fa6ae4e4d2778/img/Multichannel.png
--------------------------------------------------------------------------------
/img/Singlechannel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbonaime/seedlink_plotter/300ae9057e8bca38a9246acc666fa6ae4e4d2778/img/Singlechannel.png
--------------------------------------------------------------------------------
/img/plot_line.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbonaime/seedlink_plotter/300ae9057e8bca38a9246acc666fa6ae4e4d2778/img/plot_line.png
--------------------------------------------------------------------------------
/seedlink_plotter/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbonaime/seedlink_plotter/300ae9057e8bca38a9246acc666fa6ae4e4d2778/seedlink_plotter/__init__.py
--------------------------------------------------------------------------------
/seedlink_plotter/favicon.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbonaime/seedlink_plotter/300ae9057e8bca38a9246acc666fa6ae4e4d2778/seedlink_plotter/favicon.gif
--------------------------------------------------------------------------------
/seedlink_plotter/seedlink_plotter.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from __future__ import print_function
3 |
4 | import matplotlib
5 | # Set the backend for matplotlib.
6 | matplotlib.use("TkAgg")
7 | matplotlib.rc('figure.subplot', hspace=0)
8 | matplotlib.rc('font', family="monospace")
9 | import tkinter
10 |
11 | from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
12 | from matplotlib.figure import Figure
13 | from matplotlib.ticker import MaxNLocator
14 | from matplotlib.patheffects import withStroke
15 | from matplotlib.dates import date2num
16 | import matplotlib.pyplot as plt
17 | from obspy import Stream, Trace
18 | from obspy import __version__ as OBSPY_VERSION
19 | from obspy.core import UTCDateTime
20 | from obspy.core.event import Catalog
21 | from obspy.core.util import MATPLOTLIB_VERSION
22 | from argparse import ArgumentParser,ArgumentDefaultsHelpFormatter
23 | from math import sin
24 | import threading
25 | import time
26 | import warnings
27 | import os
28 | import sys
29 | from urllib.request import URLError
30 | import logging
31 | import numpy as np
32 |
33 |
34 | range_func = range
35 | input_func = input
36 |
37 |
38 | OBSPY_VERSION = [int(x) for x in OBSPY_VERSION.split(".")[:2]]
39 | # check obspy version and warn if it's below 0.10.0, which means that a memory
40 | # leak is present in the used seedlink client (unless working on some master
41 | # branch version after obspy/obspy@5ce975c3710ca, which is impossible to check
42 | # reliably). see #7 and obspy/obspy#918.
43 | # imports depend of the obspy version
44 | if OBSPY_VERSION < [0, 10]:
45 | warning_msg = (
46 | "ObsPy version < 0.10.0 has a memory leak in SeedLink Client. "
47 | "Please update your ObsPy installation to avoid being affected by "
48 | "the memory leak (see "
49 | "https://github.com/bonaime/seedlink_plotter/issues/7).")
50 | warnings.warn(warning_msg)
51 | sys.exit()
52 |
53 | from obspy.clients.seedlink.slpacket import SLPacket
54 | from obspy.clients.seedlink import SLClient
55 | from obspy.clients.fdsn import Client
56 |
57 | # Compatibility checks
58 | # UTCDateTime
59 | try:
60 | UTCDateTime.format_seedlink
61 | except AttributeError:
62 | # create the new format_seedlink fonction using the old formatSeedLink
63 | # method
64 | def format_seedlink(self):
65 | return self.formatSeedLink()
66 | # add the function in the class
67 | setattr(UTCDateTime, 'format_seedlink', format_seedlink)
68 | # SLPacket
69 | try:
70 | SLPacket.get_type
71 | except AttributeError:
72 | # create the new get_type fonction using the old getType method
73 | def get_type(self):
74 | return self.getType()
75 | # add the function in the class
76 | setattr(SLPacket, 'get_type', get_type)
77 |
78 | try:
79 | SLPacket.get_trace
80 | except AttributeError:
81 | # create the new get_trace fonction using the old getTrace method
82 | def get_trace(self):
83 | return self.getTrace()
84 | # add the function in the class
85 | setattr(SLPacket, 'get_trace', get_trace)
86 |
87 |
88 | class SeedlinkPlotter(tkinter.Tk):
89 |
90 | """
91 | This module plots realtime seismic data from a Seedlink server
92 | """
93 |
94 | def __init__(self, stream=None, events=None, myargs=None, lock=None,
95 | drum_plot=True, trace_ids=None, *args, **kwargs):
96 | tkinter.Tk.__init__(self, *args, **kwargs)
97 | favicon = tkinter.PhotoImage(
98 | file=os.path.join(os.path.dirname(os.path.abspath(__file__)),
99 | "favicon.gif"))
100 | self.tk.call('wm', 'iconphoto', self._w, favicon)
101 | self.wm_title("seedlink-plotter {}".format(myargs.seedlink_server))
102 | self.focus_set()
103 | self._bind_keys()
104 | args = myargs
105 | self.lock = lock
106 | ### size and position
107 | self.geometry(str(args.x_size) + 'x' + str(args.y_size) + '+' + str(
108 | args.x_position) + '+' + str(args.y_position))
109 | w, h, pad = self.winfo_screenwidth(), self.winfo_screenheight(), 3
110 | self._geometry = ("%ix%i+0+0" % (w - pad, h - pad))
111 | # hide the window decoration
112 | if args.without_decoration:
113 | self.wm_overrideredirect(True)
114 | if args.fullscreen:
115 | self._toggle_fullscreen(None)
116 |
117 | # main figure
118 | self.figure = Figure()
119 | canvas = FigureCanvasTkAgg(self.figure, master=self)
120 |
121 | if MATPLOTLIB_VERSION[:2] >= [2, 2]:
122 | canvas.draw()
123 | else:
124 | canvas.show()
125 | canvas.get_tk_widget().pack(fill=tkinter.BOTH, expand=1)
126 |
127 | self.backtrace = args.backtrace_time
128 | self.canvas = canvas
129 | self.scale = args.scale
130 | self.args = args
131 | self.stream = stream
132 | self.events = events
133 | self.drum_plot = drum_plot
134 | self.ids = trace_ids
135 |
136 | # Colors
137 | if args.rainbow:
138 | # Rainbow colors !
139 | self.color = self.rainbow_color_generator(
140 | int(args.nb_rainbow_colors))
141 | else:
142 | # Regular colors: Black, Red, Blue, Green
143 | self.color = ('#000000', '#e50000', '#0000e5', '#448630')
144 |
145 | self.plot_graph()
146 |
147 | def _quit(self, event):
148 | event.widget.quit()
149 |
150 | def _bind_keys(self):
151 | self.bind('', self._quit)
152 | self.bind('q', self._quit)
153 | self.bind('f', self._toggle_fullscreen)
154 |
155 | def _toggle_fullscreen(self, event):
156 | g = self.geometry()
157 | self.geometry(self._geometry)
158 | self._geometry = g
159 |
160 | def plot_graph(self):
161 | now = UTCDateTime()
162 | if self.drum_plot:
163 | self.stop_time = UTCDateTime(
164 | now.year, now.month, now.day, now.hour, 0, 0) + 3600
165 | self.start_time = self.stop_time - self.args.backtrace_time
166 | else:
167 | self.start_time = now - self.backtrace
168 | self.stop_time = now
169 |
170 | with self.lock:
171 | # leave some data left of our start for possible processing
172 | self.stream.trim(
173 | starttime=self.start_time - 120, nearest_sample=False)
174 | stream = self.stream.copy()
175 |
176 | try:
177 | logging.info(str(stream.split()))
178 | if not stream:
179 | raise Exception("Empty stream for plotting")
180 |
181 | if self.drum_plot :
182 | stream.merge()
183 | stream.trim(starttime=self.start_time, endtime=self.stop_time,
184 | pad=True, nearest_sample=False)
185 | else:
186 | stream.merge(-1)
187 | stream.trim(starttime=self.start_time, endtime=self.stop_time)
188 | if self.drum_plot:
189 | self.plot_drum(stream)
190 | else:
191 | self.plot_lines(stream)
192 | except Exception as e:
193 | logging.error(e)
194 | pass
195 | self.after(int(self.args.update_time * 1000), self.plot_graph)
196 |
197 | def plot_drum(self, stream):
198 | title = stream[0].id
199 | if self.scale:
200 | title += ' - scale: ' + str(self.scale) + ' -'
201 | else:
202 | title += ' - autoscale -'
203 | title += " without filtering"
204 | self.figure.clear()
205 | stream.plot(
206 | fig=self.figure, type='dayplot', interval=self.args.x_scale,
207 | number_of_ticks=self.args.time_tick_nb, tick_format=self.args.tick_format,
208 | size=(self.args.x_size, self.args.y_size),
209 | x_labels_size=8, y_labels_size=8,
210 | title=title, title_size=14,
211 | linewidth=0.5, right_vertical_labels=False,
212 | vertical_scaling_range=self.args.scale,
213 | subplots_adjust_left=0.04, subplots_adjust_right=0.99,
214 | subplots_adjust_top=0.95, subplots_adjust_bottom=0.05,
215 | one_tick_per_line=True,
216 | color=self.color,
217 | show_y_UTC_label=False,
218 | events=self.events)
219 |
220 | def plot_lines(self, stream):
221 | for id_ in self.ids:
222 | if not any([tr.id == id_ for tr in stream]):
223 | net, sta, loc, cha = id_.split(".")
224 | header = {'network': net, 'station': sta, 'location': loc,
225 | 'channel': cha, 'starttime': self.start_time}
226 | data = np.zeros(2)
227 | stream.append(Trace(data=data, header=header))
228 | stream.sort()
229 | self.figure.clear()
230 | fig = self.figure
231 | # avoid the differing trace.processing attributes prohibiting to plot
232 | # single traces of one id together.
233 | for tr in stream:
234 | tr.stats.processing = []
235 | stream.plot(fig=fig, method="fast", draw=False, equal_scale=False,
236 | size=(self.args.x_size, self.args.y_size), title="",
237 | color='Blue', tick_format=self.args.tick_format,
238 | number_of_ticks=self.args.time_tick_nb)
239 | fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
240 | bbox = dict(boxstyle="round", fc="w", alpha=0.8)
241 | path_effects = [withStroke(linewidth=4, foreground="w")]
242 | pad = 10
243 | for ax in fig.axes[::2]:
244 | if MATPLOTLIB_VERSION[0] >= 2:
245 | ax.set_facecolor("0.8")
246 | else:
247 | ax.set_axis_bgcolor("0.8")
248 | for id_, ax in zip(self.ids, fig.axes):
249 | ax.set_title("")
250 |
251 | try:
252 | text = ax.texts[0]
253 | # we should always have a single text, which is the stream
254 | # label of the axis, but catch index errors just in case
255 | except IndexError:
256 | pass
257 | else:
258 | text.set_fontsize(self.args.title_size)
259 | xlabels = ax.get_xticklabels()
260 | ylabels = ax.get_yticklabels()
261 | plt.setp(ylabels, ha="left", path_effects=path_effects)
262 | ax.yaxis.set_tick_params(pad=-pad)
263 | # treatment for bottom axes:
264 | if ax is fig.axes[-1]:
265 | plt.setp(
266 | xlabels, va="bottom", size=self.args.time_legend_size, bbox=bbox)
267 |
268 | ax.xaxis.set_tick_params(pad=-pad)
269 | # all other axes
270 | else:
271 | plt.setp(xlabels, visible=False)
272 | locator = MaxNLocator(nbins=4, prune="both")
273 | ax.yaxis.set_major_locator(locator)
274 | ax.yaxis.grid(False)
275 | ax.grid(True, axis="x")
276 | if len(ax.lines) == 1:
277 | ydata = ax.lines[0].get_ydata()
278 | # if station has no data we add a dummy trace and we end up in
279 | # a line with either 2 or 4 zeros (2 if dummy line is cut off
280 | # at left edge of time axis)
281 | if len(ydata) in [4, 2] and not ydata.any():
282 | plt.setp(ylabels, visible=False)
283 | if MATPLOTLIB_VERSION[0] >= 2:
284 | ax.set_facecolor("#ff6666")
285 | else:
286 | ax.set_axis_bgcolor("#ff6666")
287 | fig.axes[0].set_xlim(right=date2num(self.stop_time.datetime))
288 | fig.axes[0].set_xlim(left=date2num(self.start_time.datetime))
289 | if len(fig.axes) > 5:
290 | bbox["alpha"] = 0.6
291 | fig.text(0.99, 0.97, self.stop_time.strftime("%Y-%m-%d %H:%M:%S UTC"),
292 | ha="right", va="top", bbox=bbox, fontsize="medium")
293 | fig.canvas.draw()
294 |
295 | def rgb_to_hex(self, red_value, green_value, blue_value):
296 | """
297 | converter for the colors gradient
298 | """
299 | return '#%02X%02X%02X' % (red_value, green_value, blue_value)
300 |
301 | def rainbow_color_generator(self, max_color):
302 | """
303 | Rainbow color generator
304 | """
305 | color_list = []
306 | frequency = 0.3
307 | for compteur_lignes in range_func(max_color):
308 |
309 | red = sin(frequency * compteur_lignes * 2 + 0) * 127 + 128
310 | green = sin(frequency * compteur_lignes * 2 + 2) * 127 + 128
311 | blue = sin(frequency * compteur_lignes * 2 + 4) * 127 + 128
312 |
313 | color_list.append(
314 | self.rgb_to_hex(red_value=red, green_value=green, blue_value=blue))
315 |
316 | return tuple(color_list)
317 |
318 |
319 | class SeedlinkUpdater(SLClient):
320 |
321 | def __init__(self, stream, myargs=None, lock=None):
322 | # loglevel NOTSET delegates messages to parent logger
323 | super(SeedlinkUpdater, self).__init__()
324 | self.stream = stream
325 | self.lock = lock
326 | self.args = myargs
327 |
328 |
329 | def packet_handler(self, count, slpack):
330 | """
331 | for compatibility with obspy 0.10.3 renaming
332 | """
333 | self.packetHandler(count, slpack)
334 |
335 | def packetHandler(self, count, slpack):
336 | """
337 | Processes each packet received from the SeedLinkConnection.
338 | :type count: int
339 | :param count: Packet counter.
340 | :type slpack: :class:`~obspy.seedlink.SLPacket`
341 | :param slpack: packet to process.
342 | :return: Boolean true if connection to SeedLink server should be
343 | closed and session terminated, false otherwise.
344 | """
345 |
346 | # check if not a complete packet
347 | if slpack is None or (slpack == SLPacket.SLNOPACKET) or \
348 | (slpack == SLPacket.SLERROR):
349 | return False
350 |
351 | # get basic packet info
352 | type = slpack.get_type()
353 |
354 | # process INFO packets here
355 | if type == SLPacket.TYPE_SLINF:
356 | return False
357 | if type == SLPacket.TYPE_SLINFT:
358 | logging.info("Complete INFO:" + self.slconn.getInfoString())
359 | if self.infolevel is not None:
360 | return True
361 | else:
362 | return False
363 |
364 | # process packet data
365 | trace = slpack.get_trace()
366 | if trace is None:
367 | logging.info(
368 | self.__class__.__name__ + ": blockette contains no trace")
369 | return False
370 |
371 | # new samples add to the main stream which is then trimmed
372 | with self.lock:
373 | self.stream += trace
374 | self.stream.merge(-1)
375 | for tr in self.stream:
376 | tr.stats.processing = []
377 | return False
378 |
379 | def getTraceIDs(self):
380 | """
381 | Return a list of SEED style Trace IDs that the SLClient is trying to
382 | fetch data for.
383 | """
384 | ids = []
385 | streams = self.slconn.get_streams()
386 | for stream in streams:
387 | net = stream.net
388 | sta = stream.station
389 | selectors = stream.get_selectors()
390 | for selector in selectors:
391 | if len(selector) == 3:
392 | loc = ""
393 | else:
394 | loc = selector[:2]
395 | cha = selector[-3:]
396 | ids.append(".".join((net, sta, loc, cha)))
397 | ids.sort()
398 | return ids
399 |
400 |
401 | class EventUpdater():
402 | """
403 | Fetch list of seismic events
404 | """
405 | def __init__(self, stream, events, myargs=None, lock=None):
406 | self.stream = stream
407 | self.events = events
408 | self.args = myargs
409 | self.lock = lock
410 | warn_msg = "The resource identifier already exists and points to " + \
411 | "another object. It will now point to the object " + \
412 | "referred to by the new resource identifier."
413 | warnings.filterwarnings("ignore", warn_msg)
414 |
415 | def run(self):
416 | """
417 | Endless execution to update events. Does not terminate, to be run in a
418 | (daemon) thread.
419 | """
420 | while True:
421 | # no stream, reschedule event update in 20 seconds
422 | if not self.stream:
423 | time.sleep(20)
424 | continue
425 | try:
426 | events = self.get_events()
427 | except URLError as error:
428 | msg = "%s: %s\n" % (error.__class__.__name__, error)
429 | sys.stderr.write(msg)
430 | except Exception as error:
431 | msg = "%s: %s\n" % (error.__class__.__name__, error)
432 | sys.stderr.write(msg)
433 | else:
434 | self.update_events(events)
435 | time.sleep(self.args.events_update_time * 60)
436 |
437 | def get_events(self):
438 | """
439 | Method to fetch updated list of events to use in plot.
440 | """
441 | with self.lock:
442 | start = min([tr.stats.starttime for tr in self.stream])
443 | end = max([tr.stats.endtime for tr in self.stream])
444 | neries_emsc = Client("EMSC")
445 | events = neries_emsc.get_events(starttime=start, endtime=end,
446 | minmagnitude=self.args.events)
447 | return events
448 |
449 | def update_events(self, events):
450 | """
451 | Method to insert new events into list of events shared with the GUI.
452 | """
453 | with self.lock:
454 | self.events.clear()
455 | self.events.extend(events)
456 |
457 |
458 | def _parse_time_with_suffix_to_seconds(timestring):
459 | """
460 | Parse a string to seconds as float.
461 |
462 | If string can be directly converted to a float it is interpreted as
463 | seconds. Otherwise the following suffixes can be appended, case
464 | insensitive: "s" for seconds, "m" for minutes, "h" for hours, "d" for days.
465 |
466 | >>> _parse_time_with_suffix_to_seconds("12.6")
467 | 12.6
468 | >>> _parse_time_with_suffix_to_seconds("12.6s")
469 | 12.6
470 | >>> _parse_time_with_suffix_to_minutes("12.6m")
471 | 756.0
472 | >>> _parse_time_with_suffix_to_seconds("12.6h")
473 | 45360.0
474 |
475 | :type timestring: str
476 | :param timestring: "s" for seconds, "m" for minutes, "h" for hours, "d" for
477 | days.
478 | :rtype: float
479 | """
480 | try:
481 | return float(timestring)
482 | except:
483 | timestring, suffix = timestring[:-1], timestring[-1].lower()
484 | mult = {'s': 1.0, 'm': 60.0, 'h': 3600.0, 'd': 3600.0 * 24}[suffix]
485 | return float(timestring) * mult
486 |
487 |
488 | def _parse_time_with_suffix_to_minutes(timestring):
489 | """
490 | Parse a string to minutes as float.
491 |
492 | If string can be directly converted to a float it is interpreted as
493 | minutes. Otherwise the following suffixes can be appended, case
494 | insensitive: "s" for seconds, "m" for minutes, "h" for hours, "d" for days.
495 |
496 | >>> _parse_time_with_suffix_to_minutes("12.6")
497 | 12.6
498 | >>> _parse_time_with_suffix_to_minutes("12.6s")
499 | 0.21
500 | >>> _parse_time_with_suffix_to_minutes("12.6m")
501 | 12.6
502 | >>> _parse_time_with_suffix_to_minutes("12.6h")
503 | 756.0
504 |
505 | :type timestring: str
506 | :param timestring: "s" for seconds, "m" for minutes, "h" for hours, "d" for
507 | days.
508 | :rtype: float
509 | """
510 | try:
511 | return float(timestring)
512 | except:
513 | seconds = _parse_time_with_suffix_to_seconds(timestring)
514 | return seconds / 60.0
515 |
516 |
517 | def main():
518 | parser = ArgumentParser(prog='seedlink_plotter',
519 | description='Plot a realtime seismogram of a station',
520 | formatter_class=ArgumentDefaultsHelpFormatter)
521 | parser.add_argument(
522 | '-s', '--seedlink_streams', type=str, required=True,
523 | help='The seedlink stream selector string. It has the format '
524 | '"stream1[:selectors1],stream2[:selectors2],...", with "stream" '
525 | 'in "NETWORK"_"STATION" format and "selector" a space separated '
526 | 'list of "LOCATION""CHANNEL", e.g. '
527 | '"IU_KONO:BHE BHN,MN_AQU:HH?.D".')
528 | parser.add_argument(
529 | '--scale', type=int, help='the scale to apply on data ex:50000', required=False)
530 |
531 | # Real-time parameters
532 | parser.add_argument('--seedlink_server', type=str,
533 | help='the seedlink server to connect to with port. "\
534 | "ex: rtserver.ipgp.fr:18000 ', required=True)
535 | parser.add_argument(
536 | '--x_scale', type=_parse_time_with_suffix_to_minutes,
537 | help='the number of minute to plot per line'
538 | ' The following suffixes can be used as well: "s" for seconds, '
539 | '"m" for minutes, "h" for hours and "d" for days.',
540 | default='60m')
541 | parser.add_argument('-b', '--backtrace_time',
542 | help='the number of seconds to plot (3600=1h,86400=24h). The '
543 | 'following suffixes can be used as well: "m" for minutes, '
544 | '"h" for hours and "d" for days.', required=True,
545 | type=_parse_time_with_suffix_to_seconds,default='24h')
546 | parser.add_argument('--x_position', type=int,
547 | help='the x position of the graph', required=False, default=0)
548 | parser.add_argument('--y_position', type=int,
549 | help='the y position of the graph', required=False, default=0)
550 | parser.add_argument(
551 | '--x_size', type=int, help='the x size of the graph', required=False, default=800)
552 | parser.add_argument(
553 | '--y_size', type=int, help='the y size of the graph', required=False, default=600)
554 | parser.add_argument(
555 | '--title_size', type=int, help='the title size of each station in multichannel', required=False, default=10)
556 | parser.add_argument(
557 | '--time_legend_size', type=int, help='the size of time legend in multichannel', required=False, default=10)
558 | parser.add_argument(
559 | '--tick_format', type=str, help='the tick format of time legend ', required=False, default=None)
560 | parser.add_argument(
561 | '--time_tick_nb', type=int, help='the number of time tick', required=False)
562 | parser.add_argument(
563 | '--without-decoration', required=False, action='store_true',
564 | help=('the graph window will have no decorations. that means the '
565 | 'window is not controlled by the window manager and can only '
566 | 'be closed by killing the respective process.'))
567 | parser.add_argument(
568 | '--line_plot', help='regular real time plot for single station', required=False, action='store_true')
569 | parser.add_argument(
570 | '--rainbow', help='', required=False, action='store_true')
571 | parser.add_argument(
572 | '--nb_rainbow_colors', help='the numbers of colors for rainbow mode', required=False, default=10)
573 | parser.add_argument(
574 | '--update_time',
575 | help='time in seconds between each graphic update.'
576 | ' The following suffixes can be used as well: "s" for seconds, '
577 | '"m" for minutes, "h" for hours and "d" for days.',
578 | required=False, default=10,
579 | type=_parse_time_with_suffix_to_seconds)
580 | parser.add_argument('--events', required=False, default=None, type=float,
581 | help='plot events using obspy.neries, specify minimum magnitude')
582 | parser.add_argument(
583 | '--events_update_time', required=False, default=10,
584 | help='time in minutes between each event data update. '
585 | ' The following suffixes can be used as well: "s" for seconds, '
586 | '"m" for minutes, "h" for hours and "d" for days.',
587 | type=_parse_time_with_suffix_to_minutes)
588 | parser.add_argument('-f', '--fullscreen', default=False,
589 | action="store_true",
590 | help='set to full screen on startup')
591 | parser.add_argument('-v', '--verbose', default=False,
592 | action="store_true", dest="verbose",
593 | help='show verbose debugging output')
594 | parser.add_argument('--force', default=False, action="store_true",
595 | help='skip warning message and confirmation prompt '
596 | 'when opening a window without decoration')
597 | # parse the arguments
598 | args = parser.parse_args()
599 |
600 | if args.verbose:
601 | loglevel = logging.DEBUG
602 | else:
603 | loglevel = logging.CRITICAL
604 | logging.basicConfig(level=loglevel)
605 |
606 | # before anything else: warn user about window without decoration
607 | if args.without_decoration and not args.force:
608 | warning_ = ("Warning: You are about to open a window without "
609 | "decoration that is not controlled via your Window "
610 | "Manager. You can exit with -C (as long as you do "
611 | "not switch to another window with e.g. -)."
612 | "\n\nType 'y' to continue.. ")
613 | if input_func(warning_) != "y":
614 | print("Aborting.")
615 | sys.exit()
616 |
617 | now = UTCDateTime()
618 | stream = Stream()
619 | events = Catalog()
620 | lock = threading.Lock()
621 |
622 | # cl is the seedlink client
623 | seedlink_client = SeedlinkUpdater(stream, myargs=args, lock=lock)
624 | seedlink_client.slconn.set_sl_address(args.seedlink_server)
625 | seedlink_client.multiselect = args.seedlink_streams
626 |
627 | # tes if drum plot or line plot
628 | if any([x in args.seedlink_streams for x in ", ?*"]) or args.line_plot:
629 | drum_plot = False
630 | if args.time_tick_nb is None:
631 | args.time_tick_nb = 5
632 | if args.tick_format is None:
633 | args.tick_format = '%H:%M:%S'
634 | round_start = UTCDateTime(now.year, now.month, now.day, now.hour, 0, 0)
635 | round_start = round_start + 3600 - args.backtrace_time
636 | seedlink_client.begin_time = (round_start).format_seedlink()
637 |
638 | else:
639 | drum_plot = True
640 | if args.time_tick_nb is None:
641 | args.time_tick_nb = 13
642 | if args.tick_format is None:
643 | args.tick_format = '%d/%m/%y %Hh'
644 | seedlink_client.begin_time = (now - args.backtrace_time).format_seedlink()
645 |
646 | seedlink_client.initialize()
647 | ids = seedlink_client.getTraceIDs()
648 | # start cl in a thread
649 | thread = threading.Thread(target=seedlink_client.run)
650 | thread.setDaemon(True)
651 | thread.start()
652 |
653 | # start another thread for event updating if requested
654 | if args.events is not None:
655 | event_updater = EventUpdater(
656 | stream=stream, events=events, myargs=args, lock=lock)
657 | thread = threading.Thread(target=event_updater.run)
658 | thread.setDaemon(True)
659 | thread.start()
660 |
661 | # Wait few seconds to get data for the first plot
662 | time.sleep(2)
663 |
664 | master = SeedlinkPlotter(stream=stream, events=events, myargs=args,
665 | lock=lock, drum_plot=drum_plot,
666 | trace_ids=ids)
667 | master.mainloop()
668 |
669 | if __name__ == '__main__':
670 | main()
671 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | INSTALL_REQUIRES = [
4 | 'numpy',
5 | 'scipy',
6 | 'matplotlib>=2.0.0',
7 | 'obspy>=1.3.0']
8 |
9 | setup(
10 | name="seedlink-plotter",
11 | version="0.2.1",
12 | description="Plot data acquired in realtime in from a seedlink server.",
13 | author="Sebastien Bonaime, Lion Krischer, Tobias Megies",
14 | author_email="bonaime@ipgp.fr",
15 | url="https://github.com/bonaime/seedlink_plotter",
16 | download_url="https://github.com/bonaime/seedlink_plotter.git",
17 | install_requires=INSTALL_REQUIRES,
18 | python_requires='>3.7.0',
19 | keywords=["Seedlink", "ObsPy", "Seismology", "Plotting", "Realtime"],
20 | packages=["seedlink_plotter"],
21 | package_data={'seedlink_plotter': ['favicon.gif']},
22 | entry_points={
23 | 'console_scripts':
24 | ['seedlink-plotter = seedlink_plotter.seedlink_plotter:main'],
25 | },
26 | classifiers=[
27 | "Programming Language :: Python",
28 | "Development Status :: 4 - Beta",
29 | "Intended Audience :: Developers",
30 | "License :: OSI Approved :: GNU Library or " +
31 | "Lesser General Public License (LGPL)",
32 | "Operating System :: OS Independent",
33 | "Topic :: Software Development :: Libraries :: Python Modules",
34 | ],
35 | long_description="""\
36 | Seedlink-plotter is a tool to plot seismological waveform data acquired from a
37 | Seedlink server. The plots are continuously updated as recent data is acquired
38 | in realtime.
39 | """
40 | )
41 |
--------------------------------------------------------------------------------