Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • software_public/passoft/sohstationviewer
1 result
Show changes
Commits on Source (30)
Showing
with 236 additions and 153 deletions
...@@ -57,3 +57,27 @@ database editor ...@@ -57,3 +57,27 @@ database editor
-------- --------
* Fix a bug that happens when building on some Linux machines * Fix a bug that happens when building on some Linux machines
* Bux fixes * Bux fixes
2024.3.1.0
--------
* Improve performance for RT130 data sets when only a subset of all data
streams is read.
* Add the ability to zoom out in the plot.
* Add unused SOH channels (VGT, VPH, VPO, VPP, VPV) for Pegasus datalogger to
the database. These channels are used for an unsupported model of the Pegasus
datalogger.
* Plot points with value 255 in the VAN channel for Pegasus datalogger in gray
instead of red. This distinguish them from points with value 2.
* Limit the zoom range. This fixes a bug that happens when the zoom range is
too small.
* Remove support for matplotlib versions below 3.6.2.
* Pause support for numpy versions 2.0 and above. Support will resume when all
dependencies have been updated to support numpy 2.0 and above.
* Fix the button to plot a different station in the same data set not being
enabled when needed.
* Fix default rows in the channel preference dialog being converted to
non-default rows on save.
* Fix artifacts showing up in the plot with some field laptops.
* Fix problems reading RT130 data sets caused by choosing specific data streams
to read.
* Fix TPS plot being broken for data sets with high-amplitude waveforms.
package: package:
name: sohviewer name: sohviewer
version: 2024.3.0.1 version: 2024.3.1.0
source: source:
path: ../ path: ../
...@@ -15,10 +15,10 @@ requirements: ...@@ -15,10 +15,10 @@ requirements:
- pip - pip
run: run:
- python >=3.9 - python >=3.9
- numpy>=1.23.0 - numpy >=1.23.0,<2.0
- obspy >=1.3.0 - obspy >=1.3.0
- PySide6>=6.5.2 - PySide6>=6.5.2
- matplotlib>=3.5.0 - matplotlib>=3.6.2
test: test:
source_files: source_files:
......
...@@ -32,10 +32,10 @@ setup( ...@@ -32,10 +32,10 @@ setup(
], ],
}, },
install_requires=[ install_requires=[
'numpy>=1.23.0', 'numpy >=1.23.0,<2.0',
'obspy>=1.3.0', 'obspy>=1.3.0',
'PySide6>=6.5.2', 'PySide6>=6.5.2',
'matplotlib>=3.5.0', 'matplotlib>=3.6.2',
], ],
setup_requires=[], setup_requires=[],
extras_require={ extras_require={
...@@ -51,6 +51,6 @@ setup( ...@@ -51,6 +51,6 @@ setup(
name='sohviewer', name='sohviewer',
packages=find_packages(include=['sohstationviewer*']), packages=find_packages(include=['sohstationviewer*']),
url='https://git.passcal.nmt.edu/software_public/passoft/sohstationviewer', url='https://git.passcal.nmt.edu/software_public/passoft/sohstationviewer',
version='2024.3.0.1', version='2024.3.1.0',
zip_safe=False, zip_safe=False,
) )
...@@ -5,8 +5,8 @@ from typing import Literal ...@@ -5,8 +5,8 @@ from typing import Literal
ROOT_PATH = Path(__file__).resolve().parent.parent ROOT_PATH = Path(__file__).resolve().parent.parent
# The current version of SOHStationViewer # The current version of SOHStationViewer
SOFTWARE_VERSION = '2024.3.0.1' SOFTWARE_VERSION = '2024.3.1.0'
BUILD_TIME = "March 13, 2024" BUILD_TIME = "August 15, 2024"
# waveform pattern # waveform pattern
WF_1ST = 'A-HLM-V' WF_1ST = 'A-HLM-V'
......
No preview for this file type
import re
from typing import Dict, List from typing import Dict, List
from sohstationviewer.conf.constants import ColorMode from sohstationviewer.conf.constants import ColorMode
...@@ -26,7 +27,15 @@ def get_chan_plot_info(org_chan_id: str, data_type: str, ...@@ -26,7 +27,15 @@ def get_chan_plot_info(org_chan_id: str, data_type: str,
chan = convert_actual_channel_to_db_channel_w_question_mark(chan, chan = convert_actual_channel_to_db_channel_w_question_mark(chan,
data_type) data_type)
if len(org_chan_id) == 3 and org_chan_id.startswith('DS'): # RT130 waveform channels is formed by the string DS followed by the data
# stream number, a dash character, and the channel number.
# Doing incomplete checks for RT130 waveform channels have caused a bunch
# of problems before (see https://git.passcal.nmt.edu/software_public/passoft/sohstationviewer/-/merge_requests/240?diff_id=5092&start_sha=55ac101ffac8860ba66ac5e7b694e70ee397919c # noqa
# and https://git.passcal.nmt.edu/software_public/passoft/sohstationviewer/-/issues/287). # noqa
# So, we match RT130 waveform channels exactly with a regex to avoid these
# problems.
rt130_waveform_regex = re.compile(r'DS\d-\d')
if rt130_waveform_regex.match(org_chan_id):
chan = 'SEISMIC' chan = 'SEISMIC'
if dbConf['seisRE'].match(chan): if dbConf['seisRE'].match(chan):
chan = 'SEISMIC' chan = 'SEISMIC'
......
No preview for this file type
...@@ -16,7 +16,9 @@ all the TPS channels. Ctr/cmd + Click on TPS plot will work the same. ...@@ -16,7 +16,9 @@ all the TPS channels. Ctr/cmd + Click on TPS plot will work the same.
--------------------------- ---------------------------
# Zooming # Zooming
Shift + Click to mark the first zooming point, then Shift + click again to mark
## Zoom in
Shift + Click on a plot to mark the first zooming point, then Shift + click again to mark
the second zooming point to zoom the area between two zooming points. the second zooming point to zoom the area between two zooming points.
The zooming task will be performed in the area corresponding to zooming triggering The zooming task will be performed in the area corresponding to zooming triggering
...@@ -31,3 +33,7 @@ be triggered from TPS dialog. ...@@ -31,3 +33,7 @@ be triggered from TPS dialog.
<br /> <br />
<br /> <br />
<br /> <br />
## Zoom out
Shift + Click on the left side of a plot to zoom out one level, which means
undoing one zoom in.
\ No newline at end of file
...@@ -269,7 +269,11 @@ class RT130(GeneralData): ...@@ -269,7 +269,11 @@ class RT130(GeneralData):
def read_file(self, path2file: Path, file_name: str, def read_file(self, path2file: Path, file_name: str,
count: int, total: int) -> None: count: int, total: int) -> None:
""" """
Read data or text from file Read data or text from file.
Skip reading data if data belong to data stream that isn't requested.
Data stream can be detected based on folder structure:
[YYYYDOY]/[das_serial_number]/[data_stream]/[filename]
:param path2file: absolute path to file :param path2file: absolute path to file
:param file_name: name of file :param file_name: name of file
:param count: total number of file read :param count: total number of file read
...@@ -282,6 +286,14 @@ class RT130(GeneralData): ...@@ -282,6 +286,14 @@ class RT130(GeneralData):
if log_text is not None: if log_text is not None:
self.log_texts['TEXT'].append(log_text) self.log_texts['TEXT'].append(log_text)
return return
if self.req_data_streams != ['*']:
# filter non selected data stream
ds = path2file.parts[-2]
if ds in ['1', '2', '3', '4', '5', '6', '7', '8']:
# Check if folder name match format of data stream's folder
if int(ds) not in self.req_data_streams:
# Check if data stream is required
return
self.read_reftek_130(path2file) self.read_reftek_130(path2file)
def select_data_set_id(self) -> Tuple[str, str]: def select_data_set_id(self) -> Tuple[str, str]:
......
...@@ -335,7 +335,7 @@ class ChannelPreferDialog(OneWindowAtATimeDialog): ...@@ -335,7 +335,7 @@ class ChannelPreferDialog(OneWindowAtATimeDialog):
self.soh_list_table_widget.item( self.soh_list_table_widget.item(
count, COL['preferredSOHs']).setText(r['preferredSOHs']) count, COL['preferredSOHs']).setText(r['preferredSOHs'])
if r['default'] == 1: if r['isDefault'] == 1:
self.set_default_row(count) self.set_default_row(count)
if r['current'] == 1: if r['current'] == 1:
...@@ -508,9 +508,13 @@ class ChannelPreferDialog(OneWindowAtATimeDialog): ...@@ -508,9 +508,13 @@ class ChannelPreferDialog(OneWindowAtATimeDialog):
""" """
selected_row_idx = self.soh_list_table_widget.indexFromItem( selected_row_idx = self.soh_list_table_widget.indexFromItem(
self.soh_list_item self.soh_list_item
).row()
row_edit_button = self.soh_list_table_widget.cellWidget(
selected_row_idx, COL['edit']
) )
# Hardcoding the check for default row because there is no time. is_default_row = not row_edit_button.isEnabled()
if selected_row_idx.row() < 4:
if is_default_row:
err_msg = ('The selected row is a default row and cannot be ' err_msg = ('The selected row is a default row and cannot be '
'overwritten. Please select a non-default row and try ' 'overwritten. Please select a non-default row and try '
'again.') 'again.')
...@@ -732,6 +736,12 @@ class ChannelPreferDialog(OneWindowAtATimeDialog): ...@@ -732,6 +736,12 @@ class ChannelPreferDialog(OneWindowAtATimeDialog):
row_idx, COL['dataType']).currentText() row_idx, COL['dataType']).currentText()
preferred_sohs = self.soh_list_table_widget.item( preferred_sohs = self.soh_list_table_widget.item(
row_idx, COL['preferredSOHs']).text() row_idx, COL['preferredSOHs']).text()
row_edit_button = self.soh_list_table_widget.cellWidget(
row_idx, COL['edit']
)
# The edit button is only disabled for default rows, so we can use its
# state to determine if a row is a default row.
is_default_row = not row_edit_button.isEnabled()
if preferred_sohs.strip() == '' and name.strip() == '': if preferred_sohs.strip() == '' and name.strip() == '':
return '', '', '' return '', '', ''
display_id = row_idx + 1 display_id = row_idx + 1
...@@ -749,8 +759,10 @@ class ChannelPreferDialog(OneWindowAtATimeDialog): ...@@ -749,8 +759,10 @@ class ChannelPreferDialog(OneWindowAtATimeDialog):
QtWidgets.QMessageBox.information(self, "Missing info", msg) QtWidgets.QMessageBox.information(self, "Missing info", msg)
return return
sql = (f"INSERT INTO ChannelPrefer (name, preferredSOHs, dataType, " sql = (f"INSERT INTO ChannelPrefer (name, preferredSOHs, dataType, "
f"current) VALUES " f"current, isDefault) VALUES "
f"('{name}', '{preferred_sohs}', '{data_type}', {current})") f"('{name}', '{preferred_sohs}', '{data_type}', {current}, "
f"{is_default_row})")
print(sql)
primary_key = name primary_key = name
return primary_key, str(display_id), sql return primary_key, str(display_id), sql
......
...@@ -67,16 +67,6 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): ...@@ -67,16 +67,6 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
super().__init__(parent) super().__init__(parent)
self.setup_ui(self) self.setup_ui(self)
""" """
SCREEN INFO
actual_dpi: actual dpi of the screen where main_window is located.
actual_dpi = physical_dpi * device_pixel_ratio
dpi_y: vertical dpi of the screen
dpi_x: horizontal dpi of the screen
"""
self.actual_dpi: float = 100.
self.dpi_x: float = 25.
self.dpi_y: float = 30.
"""
dir_names: list of absolute paths of data sets dir_names: list of absolute paths of data sets
""" """
self.list_of_dir: List[Path] = [] self.list_of_dir: List[Path] = []
...@@ -617,19 +607,6 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): ...@@ -617,19 +607,6 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
msg = "No directories have been selected." msg = "No directories have been selected."
raise Exception(msg) raise Exception(msg)
try:
self.req_wf_chans = self.get_requested_wf_chans()
except Exception as e:
QMessageBox.information(self, "Waveform Selection", str(e))
self.cancel_loading()
return
if self.warn_big_file_sizes.isChecked():
# call check_folder_size() here b/c it requires list_of_dir and it
# is before the called for detect_data_type() which sometimes take
# quite a long time.
if not check_folders_size(self.list_of_dir, self.req_wf_chans):
raise Exception("Big size")
# Log files don't have a data type that can be detected, so we don't # Log files don't have a data type that can be detected, so we don't
# detect the data type if we are reading them. # detect the data type if we are reading them.
if self.rt130_das_dict == {} and not self.log_checkbox.isChecked(): if self.rt130_das_dict == {} and not self.log_checkbox.isChecked():
...@@ -643,6 +620,21 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): ...@@ -643,6 +620,21 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
"Do you want to cancel to select different folder(s)\n" "Do you want to cancel to select different folder(s)\n"
"Or continue to read any available mseed file?") "Or continue to read any available mseed file?")
raise Exception(msg) raise Exception(msg)
try:
# get_requested_wf_chans have to be called after data_type is
# detected
self.req_wf_chans = self.get_requested_wf_chans()
except Exception as e:
QMessageBox.information(self, "Waveform Selection", str(e))
self.cancel_loading()
return
if self.warn_big_file_sizes.isChecked():
# call check_folder_size() here b/c it requires list_of_dir and it
# is before the called for detect_data_type() which sometimes take
# quite a long time.
if not check_folders_size(self.list_of_dir, self.req_wf_chans):
raise Exception("Big size")
def clear_plots(self): def clear_plots(self):
self.plotting_widget.clear() self.plotting_widget.clear()
...@@ -1057,7 +1049,7 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): ...@@ -1057,7 +1049,7 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
self.is_stopping = False self.is_stopping = False
try: try:
if len(self.data_object.data_set_ids) > 1: if len(self.data_object.data_set_ids) > 1:
self.plot_diff_data_set_id.setEnabled(True) self.plot_diff_data_set_id_button.setEnabled(True)
except AttributeError: except AttributeError:
pass pass
...@@ -1409,19 +1401,6 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): ...@@ -1409,19 +1401,6 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
self.open_files_list.insertItem(0, found_files_list_item) self.open_files_list.insertItem(0, found_files_list_item)
# ======================== EVENTS ========================== # ======================== EVENTS ==========================
def moveEvent(self, event):
"""
Get dpi, dpi_x, dpi_y whenever main window is moved
"""
screen = QtWidgets.QApplication.screenAt(event.pos())
if screen is not None:
curr_actual_dpi = (
screen.physicalDotsPerInch() * screen.devicePixelRatio())
self.dpi_x = screen.physicalDotsPerInchX()
self.dpi_y = screen.physicalDotsPerInchY()
self.actual_dpi = curr_actual_dpi
return super().moveEvent(event)
def resizeEvent(self, event): def resizeEvent(self, event):
""" """
......
...@@ -234,8 +234,8 @@ class MultiThreadedPlottingWidget(PlottingWidget): ...@@ -234,8 +234,8 @@ class MultiThreadedPlottingWidget(PlottingWidget):
channel_processor.stopped.connect(self.has_stopped) channel_processor.stopped.connect(self.has_stopped)
def plot_channels( def plot_channels(
self, d_obj, data_set_id, start_tm, end_tm, self, d_obj, data_set_id, start_tm=None, end_tm=None,
time_ticks_total, pref_order=[]): time_ticks_total=0, pref_order=[], keep_zoom=False):
""" """
Prepare to plot waveform/SOH/mass-position data: Prepare to plot waveform/SOH/mass-position data:
+ get_plotting_info: get sizing info + get_plotting_info: get sizing info
...@@ -256,6 +256,8 @@ class MultiThreadedPlottingWidget(PlottingWidget): ...@@ -256,6 +256,8 @@ class MultiThreadedPlottingWidget(PlottingWidget):
self.start_tm = start_tm self.start_tm = start_tm
self.end_tm = end_tm self.end_tm = end_tm
self.time_ticks_total = time_ticks_total self.time_ticks_total = time_ticks_total
if not keep_zoom:
self.zoom_minmax_list = []
if 'VST' in pref_order: if 'VST' in pref_order:
# pref_order use original name VST to read from file. # pref_order use original name VST to read from file.
......
""" """
Class of which object is used to plot data Class of which object is used to plot data
""" """
from typing import List, Optional, Union from typing import List, Optional, Union, Tuple
import math import math
import numpy as np import numpy as np
import matplotlib.text import matplotlib.text
...@@ -52,6 +52,16 @@ class PlottingWidget(QtWidgets.QScrollArea): ...@@ -52,6 +52,16 @@ class PlottingWidget(QtWidgets.QScrollArea):
self.name = name self.name = name
self.tracking_box = tracking_box self.tracking_box = tracking_box
# =============== declare attributes ======================= # =============== declare attributes =======================
"""
DPI INFO
actual_dpi: actual dpi of the screen where main_window is located.
actual_dpi = physical_dpi * device_pixel_ratio
dpi_y: vertical dpi of the screen
dpi_x: horizontal dpi of the screen
"""
self.actual_dpi: float = 100.
self.dpi_x: float = 25.
self.dpi_y: float = 30.
""" """
has_data: indicate if there're any data to be plotted has_data: indicate if there're any data to be plotted
""" """
...@@ -71,6 +81,12 @@ class PlottingWidget(QtWidgets.QScrollArea): ...@@ -71,6 +81,12 @@ class PlottingWidget(QtWidgets.QScrollArea):
self.min_x = 0 self.min_x = 0
self.max_x = 0 self.max_x = 0
""" """
zoom_minmax_list: list of x ranges of zooming. The first range is the
original of x ranges. If there is more than one ranges the plotting has
been zoomed in.
"""
self.zoom_minmax_list: List[Tuple[float, float]] = []
"""
plotting_bot: float - bottom of a current plot, decrease by plot_h plotting_bot: float - bottom of a current plot, decrease by plot_h
return from self.get_height() whenever a new plot is added return from self.get_height() whenever a new plot is added
""" """
...@@ -241,6 +257,18 @@ class PlottingWidget(QtWidgets.QScrollArea): ...@@ -241,6 +257,18 @@ class PlottingWidget(QtWidgets.QScrollArea):
top_space_in + bot_space_in + top_space_in + bot_space_in +
2 * time_bar_height_in) 2 * time_bar_height_in)
def set_dpi(self):
"""
Calculate dpi to correct sizing calculation
"""
screen = self.screen()
if screen is not None:
self.dpi_x = screen.physicalDotsPerInchX()
self.dpi_y = screen.physicalDotsPerInchY()
# Using dpi_x as standard
self.actual_dpi = (screen.physicalDotsPerInchX() *
self.devicePixelRatio())
def set_size(self, view_port_size: Optional[float] = None) -> None: def set_size(self, view_port_size: Optional[float] = None) -> None:
""" """
Set figure's width and main widget's width to fit the width of the Set figure's width and main widget's width to fit the width of the
...@@ -251,13 +279,14 @@ class PlottingWidget(QtWidgets.QScrollArea): ...@@ -251,13 +279,14 @@ class PlottingWidget(QtWidgets.QScrollArea):
:param view_port_size: size of viewport :param view_port_size: size of viewport
""" """
self.set_dpi()
if view_port_size is None: if view_port_size is None:
view_port_size = self.maximumViewportSize() view_port_size = self.maximumViewportSize()
view_port_width = view_port_size.width() view_port_width = view_port_size.width()
self.plotting_axes.canvas.setFixedWidth(view_port_width) self.plotting_axes.canvas.setFixedWidth(view_port_width)
self.fig_width_in = view_port_width/self.parent.dpi_x self.fig_width_in = view_port_width/self.dpi_x
self.plotting_axes.fig.set_dpi(self.parent.actual_dpi) self.plotting_axes.fig.set_dpi(self.actual_dpi)
# set view size fit with the scroll's viewport size # set view size fit with the scroll's viewport size
self.main_widget.setFixedWidth(view_port_size.width()) self.main_widget.setFixedWidth(view_port_size.width())
...@@ -265,7 +294,7 @@ class PlottingWidget(QtWidgets.QScrollArea): ...@@ -265,7 +294,7 @@ class PlottingWidget(QtWidgets.QScrollArea):
view_port_height = view_port_size.height() view_port_height = view_port_size.height()
self.main_widget.setFixedHeight(view_port_height) self.main_widget.setFixedHeight(view_port_height)
self.plotting_axes.canvas.setFixedHeight(view_port_height) self.plotting_axes.canvas.setFixedHeight(view_port_height)
fig_height_in = view_port_height/self.parent.dpi_y fig_height_in = view_port_height/self.dpi_y
self.plotting_axes.fig.set_figheight(fig_height_in) self.plotting_axes.fig.set_figheight(fig_height_in)
def plotting_preset(self): def plotting_preset(self):
...@@ -279,7 +308,7 @@ class PlottingWidget(QtWidgets.QScrollArea): ...@@ -279,7 +308,7 @@ class PlottingWidget(QtWidgets.QScrollArea):
Calculate height_normalizing_factor. Calculate height_normalizing_factor.
""" """
self.set_size() self.set_size()
fig_height = math.ceil(self.fig_height_in * self.parent.dpi_y) fig_height = math.ceil(self.fig_height_in * self.dpi_y)
# adjusting main_widget to fit view port # adjusting main_widget to fit view port
max_height = max(self.maximumViewportSize().height(), fig_height) max_height = max(self.maximumViewportSize().height(), fig_height)
...@@ -340,11 +369,29 @@ class PlottingWidget(QtWidgets.QScrollArea): ...@@ -340,11 +369,29 @@ class PlottingWidget(QtWidgets.QScrollArea):
:param xdata: float - time value in plot :param xdata: float - time value in plot
""" """
if self.new_min_x == xdata: self.zoom_marker1.set_visible(False)
self.zoom_marker2.set_visible(False)
if abs(self.new_min_x - xdata) < 0.001:
# to prevent weird zoom in
display_tracking_info(
self.tracking_box, "Selected range is too small to zoom in.")
self.plotting_axes.canvas.draw()
return return
self.zoom_marker1_shown = False self.zoom_marker1_shown = False
[self.min_x, self.max_x] = sorted([self.new_min_x, xdata]) [self.min_x, self.max_x] = sorted([self.new_min_x, xdata])
self.set_lim() self.set_lim()
self.plotting_axes.canvas.draw()
def zoom_out(self):
"""
Zoom out by setting limit to the previous range when there's at least
one zoom-in.
"""
if len(self.zoom_minmax_list) > 1:
self.min_x, self.max_x = self.zoom_minmax_list[-2]
self.zoom_minmax_list.pop()
self.set_lim(is_zoom_in=False)
self.zoom_marker1_shown = False
self.zoom_marker1.set_visible(False) self.zoom_marker1.set_visible(False)
self.zoom_marker2.set_visible(False) self.zoom_marker2.set_visible(False)
self.plotting_axes.canvas.draw() self.plotting_axes.canvas.draw()
...@@ -477,9 +524,9 @@ class PlottingWidget(QtWidgets.QScrollArea): ...@@ -477,9 +524,9 @@ class PlottingWidget(QtWidgets.QScrollArea):
When click mouse on the current plottingWidget, SOHView will loop When click mouse on the current plottingWidget, SOHView will loop
through different plottingWidgets to do the same task for through different plottingWidgets to do the same task for
interaction: interaction:
+ shift+click: call on_shift_click() to do zooming. This is + shift+click: is disregarded if start in TimePowerSquareWidget
disregarded in TimePowerSquareWidget because it isn't subjected * If click on left side of the plot (xdata<xmin): call zoom out
to be zoomed in. * Otherwise: call on_shift_click to do tasks of zoom in
+ ctrl+click or cmd+click in mac: call on_ctrl_cmd_click() to show + ctrl+click or cmd+click in mac: call on_ctrl_cmd_click() to show
ruler ruler
...@@ -510,16 +557,6 @@ class PlottingWidget(QtWidgets.QScrollArea): ...@@ -510,16 +557,6 @@ class PlottingWidget(QtWidgets.QScrollArea):
else: else:
xdata = self.get_timestamp(event) xdata = self.get_timestamp(event)
# We only want to remove the text on the ruler when we start zooming in
# or move the ruler to another location.
if modifiers in [QtCore.Qt.KeyboardModifier.ControlModifier,
QtCore.Qt.KeyboardModifier.MetaModifier,
QtCore.Qt.KeyboardModifier.ShiftModifier]:
try:
self.ruler_text.remove()
self.ruler_text = None
except AttributeError:
pass
if (self.main_window.tps_check_box.isChecked() and if (self.main_window.tps_check_box.isChecked() and
modifiers in [QtCore.Qt.KeyboardModifier.ControlModifier, modifiers in [QtCore.Qt.KeyboardModifier.ControlModifier,
QtCore.Qt.KeyboardModifier.MetaModifier, QtCore.Qt.KeyboardModifier.MetaModifier,
...@@ -528,16 +565,14 @@ class PlottingWidget(QtWidgets.QScrollArea): ...@@ -528,16 +565,14 @@ class PlottingWidget(QtWidgets.QScrollArea):
for w in self.peer_plotting_widgets: for w in self.peer_plotting_widgets:
if not w.has_data: if not w.has_data:
continue continue
if modifiers in [QtCore.Qt.KeyboardModifier.ControlModifier,
QtCore.Qt.KeyboardModifier.MetaModifier,
QtCore.Qt.KeyboardModifier.ShiftModifier]:
try:
w.ruler_text.remove()
w.ruler_text = None
except AttributeError:
pass
if modifiers == QtCore.Qt.KeyboardModifier.ShiftModifier: if modifiers == QtCore.Qt.KeyboardModifier.ShiftModifier:
w.on_shift_click(xdata) if xdata < w.min_x:
# click on left of plot
w.zoom_marker1_shown = False # reset zoom in
w.zoom_out()
else:
w.on_shift_click(xdata)
elif modifiers in [QtCore.Qt.KeyboardModifier.ControlModifier, elif modifiers in [QtCore.Qt.KeyboardModifier.ControlModifier,
QtCore.Qt.KeyboardModifier.MetaModifier]: QtCore.Qt.KeyboardModifier.MetaModifier]:
w.on_ctrl_cmd_click(xdata) w.on_ctrl_cmd_click(xdata)
...@@ -565,6 +600,7 @@ class PlottingWidget(QtWidgets.QScrollArea): ...@@ -565,6 +600,7 @@ class PlottingWidget(QtWidgets.QScrollArea):
:param xdata: float - time value of a channel plot :param xdata: float - time value of a channel plot
""" """
self.zoom_marker1.set_visible(False) self.zoom_marker1.set_visible(False)
self.zoom_marker1_shown = False self.zoom_marker1_shown = False
try: try:
...@@ -575,6 +611,11 @@ class PlottingWidget(QtWidgets.QScrollArea): ...@@ -575,6 +611,11 @@ class PlottingWidget(QtWidgets.QScrollArea):
if xdata >= self.min_x: if xdata >= self.min_x:
ruler_text_content = format_time(xdata, self.parent.date_format, ruler_text_content = format_time(xdata, self.parent.date_format,
'HH:MM:SS') 'HH:MM:SS')
try:
# remove ruler_text before creating the new one
self.ruler_text.remove()
except AttributeError:
pass
self.ruler_text = self.plotting_axes.fig.text( self.ruler_text = self.plotting_axes.fig.text(
xdata, 5000, ruler_text_content, xdata, 5000, ruler_text_content,
verticalalignment='top', verticalalignment='top',
...@@ -587,18 +628,20 @@ class PlottingWidget(QtWidgets.QScrollArea): ...@@ -587,18 +628,20 @@ class PlottingWidget(QtWidgets.QScrollArea):
def on_shift_click(self, xdata): def on_shift_click(self, xdata):
""" """
On shift + left click: On shift + left click:
if click on the left of a plot, do zoom out to the previous range
if zoom_marker1 not shown yet: if zoom_marker1 not shown yet:
+ hide ruler
+ connect zoom_marker2 to follow mouse + connect zoom_marker2 to follow mouse
+ show zoom_marker1 + show zoom_marker1
else: else:
+ show zoom_marker2 + show zoom_marker2
+ zoom data in between 2 zoomMarkers + zoom data in between 2 zoomMarkers
Notice that ruler will stay at the same xdata either with zoom in
or out.
:param xdata: float - time value of a channel plot :param xdata: float - time value of a channel plot
""" """
if not self.zoom_marker1_shown: if not self.zoom_marker1_shown:
self.ruler.set_visible(False)
self.set_ruler_visibled(self.zoom_marker1, xdata) self.set_ruler_visibled(self.zoom_marker1, xdata)
self.new_min_x = xdata self.new_min_x = xdata
self.zoom_marker1_shown = True self.zoom_marker1_shown = True
...@@ -629,6 +672,8 @@ class PlottingWidget(QtWidgets.QScrollArea): ...@@ -629,6 +672,8 @@ class PlottingWidget(QtWidgets.QScrollArea):
When press on Escape key, hide all rulers and set False for When press on Escape key, hide all rulers and set False for
zoom_marker1_shown on all plotting widgets zoom_marker1_shown on all plotting widgets
Notice: press Escape is the only way to hide ruler and its text.
:param event: QKeyEvent - event to know what key is pressed :param event: QKeyEvent - event to know what key is pressed
""" """
if event.key() == QtCore.Qt.Key.Key_Escape: if event.key() == QtCore.Qt.Key.Key_Escape:
...@@ -673,17 +718,26 @@ class PlottingWidget(QtWidgets.QScrollArea): ...@@ -673,17 +718,26 @@ class PlottingWidget(QtWidgets.QScrollArea):
or min(gap) <= self.min_x <= max(gap) or min(gap) <= self.min_x <= max(gap)
or min(gap) <= self.max_x <= max(gap)) or min(gap) <= self.max_x <= max(gap))
def set_lim(self, first_time=False): def set_lim(self, first_time=False, is_zoom_in=True):
""" """
+ Re-decimate channel data to get more detail when zoom in + Append to zoom_minmax_list if called from a zoom_in. First time
(get_zoom_data) plotting is considered a zoom_in to create first x range in the list.
+ Update timestamp bar with new ticks info + Update timestamp bar with new ticks info
+ Update gap_bar by setting xlim and re-calculate gap total. + Update gap_bar by setting xlim and re-calculate gap total.
+ for each axes, set new x_lim, y_lim, label for totals of data points + for each axes, set new x_lim, y_lim, label for totals of data points
:param first_time: bool - flag shows that this set_lim is called the :param first_time: bool - flag shows that this set_lim is called the
fist time for this data set or not. fist time for this data set or not.
:param is_zoom_in: if set_lim comes from zoom_in task
""" """
from_add_remove_channels = False
if is_zoom_in:
if first_time and self.zoom_minmax_list:
self.min_x, self.max_x = self.zoom_minmax_list[-1]
from_add_remove_channels = True
else:
self.zoom_minmax_list.append((self.min_x, self.max_x))
self.plotting_axes.update_timestamp_bar(self.timestamp_bar_top) self.plotting_axes.update_timestamp_bar(self.timestamp_bar_top)
self.plotting_axes.update_timestamp_bar(self.timestamp_bar_bottom) self.plotting_axes.update_timestamp_bar(self.timestamp_bar_bottom)
if self.gap_bar is not None: if self.gap_bar is not None:
...@@ -703,7 +757,7 @@ class PlottingWidget(QtWidgets.QScrollArea): ...@@ -703,7 +757,7 @@ class PlottingWidget(QtWidgets.QScrollArea):
break break
ax.set_xlim(self.min_x, self.max_x) ax.set_xlim(self.min_x, self.max_x)
if not first_time: if not first_time or from_add_remove_channels:
new_min_y = None new_min_y = None
new_max_y = None new_max_y = None
if hasattr(ax, 'x_top'): if hasattr(ax, 'x_top'):
......
...@@ -4,7 +4,7 @@ from math import sqrt ...@@ -4,7 +4,7 @@ from math import sqrt
from typing import Union, Tuple, Dict from typing import Union, Tuple, Dict
from PySide6 import QtWidgets, QtCore from PySide6 import QtWidgets, QtCore
from PySide6.QtCore import QEventLoop, Qt, QSize from PySide6.QtCore import QEventLoop, Qt
from PySide6.QtGui import QCursor from PySide6.QtGui import QCursor
from PySide6.QtWidgets import QApplication, QTabWidget from PySide6.QtWidgets import QApplication, QTabWidget
...@@ -38,18 +38,6 @@ class TimePowerSquaredDialog(QtWidgets.QWidget): ...@@ -38,18 +38,6 @@ class TimePowerSquaredDialog(QtWidgets.QWidget):
super().__init__() super().__init__()
self.main_window = parent self.main_window = parent
""" """
SCREEN INFO
screen_size: size of screen where tps dialog is located
actual_dpi: actual dpi of the screen where tps dialog is located.
actual_dpi = physical_dpi * device_pixel_ratio
dpi_y: vertical dpi of the screen
dpi_x: horizontal dpi of the screen
"""
self.screen_size: QSize = None
self.actual_dpi: float = 100.
self.dpi_x: float = 25.
self.dpi_y: float = 30.
"""
data_type: str - type of data being plotted data_type: str - type of data being plotted
""" """
self.data_type = None self.data_type = None
...@@ -203,20 +191,6 @@ class TimePowerSquaredDialog(QtWidgets.QWidget): ...@@ -203,20 +191,6 @@ class TimePowerSquaredDialog(QtWidgets.QWidget):
pass pass
return super(TimePowerSquaredDialog, self).resizeEvent(event) return super(TimePowerSquaredDialog, self).resizeEvent(event)
def moveEvent(self, event):
"""
Get actual dpi, dpi_x, dpi_y whenever tps dialog is moved
"""
screen = QtWidgets.QApplication.screenAt(event.pos())
if screen is not None:
curr_actual_dpi = (
screen.physicalDotsPerInch() * screen.devicePixelRatio())
self.dpi_x = screen.physicalDotsPerInchX()
self.dpi_y = screen.physicalDotsPerInchY()
self.actual_dpi = curr_actual_dpi
return super().moveEvent(event)
def connect_signals(self): def connect_signals(self):
""" """
Connect functions to widgets Connect functions to widgets
......
...@@ -109,9 +109,10 @@ def get_tps_for_discontinuous_data( ...@@ -109,9 +109,10 @@ def get_tps_for_discontinuous_data(
mean_squares = np.zeros_like(start_5min_blocks, dtype=float) mean_squares = np.zeros_like(start_5min_blocks, dtype=float)
# Calculate mean_square for each 5m block # Calculate mean_square for each 5m block
# adding dtype=float64 to prevent integer overflow
for i, d in enumerate(split_data): for i, d in enumerate(split_data):
if len(d) != 0: if len(d) != 0:
mean_squares[i] = np.mean(np.square(d)) mean_squares[i] = np.mean(np.square(d, dtype='float64'))
elif ((0 < i < len(split_data) - 1) and elif ((0 < i < len(split_data) - 1) and
len(split_data[i - 1]) > 0 and len(split_data[i + 1]) > 0): len(split_data[i - 1]) > 0 and len(split_data[i + 1]) > 0):
""" """
...@@ -119,7 +120,8 @@ def get_tps_for_discontinuous_data( ...@@ -119,7 +120,8 @@ def get_tps_for_discontinuous_data(
data in the previous and next blocks if they both have data. data in the previous and next blocks if they both have data.
""" """
mean_squares[i] = np.mean(np.square( mean_squares[i] = np.mean(np.square(
np.hstack((split_data[i - 1], split_data[i + 1])) np.hstack((split_data[i - 1], split_data[i + 1])),
dtype='float64'
)) ))
# reshape 1D mean_quares into 2D array in which each row contains 288 of # reshape 1D mean_quares into 2D array in which each row contains 288 of
# 5m blocks' mean_squares of a day # 5m blocks' mean_squares of a day
......
...@@ -556,7 +556,7 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget): ...@@ -556,7 +556,7 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget):
self.zoom_marker1_shown = False self.zoom_marker1_shown = False
# locate the ruler on each channel in the tab to highlight the point # locate the ruler on each channel in the tab to highlight the point
for rl in self.rulers: for rl in self.rulers:
rl.set_data(self.parent.five_minute_idx, - self.parent.day_idx) rl.set_data([self.parent.five_minute_idx], [-self.parent.day_idx])
def on_shift_click(self, xdata): def on_shift_click(self, xdata):
""" """
...@@ -579,6 +579,16 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget): ...@@ -579,6 +579,16 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget):
self.set_lim_markers() self.set_lim_markers()
self.zoom_marker1_shown = False self.zoom_marker1_shown = False
def zoom_out(self):
"""
Zoom out by setting limit to the previous range when there's at least
one zoom-in.
"""
if len(self.zoom_minmax_list) > 1:
self.min_x, self.max_x = self.zoom_minmax_list[-2]
self.zoom_minmax_list.pop()
self.set_lim_markers(is_zoom_in=False)
def set_rulers_invisible(self): def set_rulers_invisible(self):
""" """
Clear data for self.rulers to make them disappeared. Clear data for self.rulers to make them disappeared.
...@@ -586,22 +596,30 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget): ...@@ -586,22 +596,30 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget):
for rl in self.rulers: for rl in self.rulers:
rl.set_data([], []) rl.set_data([], [])
def set_lim_markers(self): def set_lim_markers(self, is_zoom_in=True):
""" """
Find x index (which index in five minutes of a day) and + Append to zoom_minmax_list if called from a zoom_in. First time
y index (which day) of self.min_x and self.min_y, and set data for plotting is considered a zoom_in to create first x range in the list.
all markers in self.zoom_marker1s and self.zoom_marker2s.
+ Find x index (which index in five minutes of a day) and
y index (which day) of self.min_x and self.min_y, and set data for
all markers in self.zoom_marker1s and self.zoom_marker2s.
:param is_zoom_in: if set_lim comes from zoom_in task
""" """
if is_zoom_in:
self.zoom_minmax_list.append((self.min_x, self.max_x))
five_minute_idx, day_idx = find_tps_tm_idx(self.min_x, five_minute_idx, day_idx = find_tps_tm_idx(self.min_x,
self.start_5min_blocks, self.start_5min_blocks,
self.start_first_day) self.start_first_day)
for zm1 in self.zoom_marker1s: for zm1 in self.zoom_marker1s:
zm1.set_data(five_minute_idx, - day_idx) zm1.set_data([five_minute_idx], [-day_idx])
five_minute_idx, day_idx = find_tps_tm_idx(self.max_x, five_minute_idx, day_idx = find_tps_tm_idx(self.max_x,
self.start_5min_blocks, self.start_5min_blocks,
self.start_first_day) self.start_first_day)
for zm2 in self.zoom_marker2s: for zm2 in self.zoom_marker2s:
zm2.set_data(five_minute_idx, - day_idx) zm2.set_data([five_minute_idx], [-day_idx])
def request_stop(self): def request_stop(self):
"""Request all running channel processors to stop.""" """Request all running channel processors to stop."""
......
...@@ -68,16 +68,6 @@ class WaveformDialog(QtWidgets.QWidget): ...@@ -68,16 +68,6 @@ class WaveformDialog(QtWidgets.QWidget):
""" """
self.parent = parent self.parent = parent
""" """
SCREEN INFO
actual_dpi: actual dpi of the screen where waveform dialog is located.
actual_dpi = physical_dpi * device_pixel_ratio
dpi_y: vertical dpi of the screen
dpi_x: horizontal dpi of the screen
"""
self.actual_dpi: float = 100.
self.dpi_x: float = 25.
self.dpi_y: float = 30.
"""
data_type: str - type of data being plotted data_type: str - type of data being plotted
""" """
self.data_type = None self.data_type = None
...@@ -139,20 +129,6 @@ class WaveformDialog(QtWidgets.QWidget): ...@@ -139,20 +129,6 @@ class WaveformDialog(QtWidgets.QWidget):
self.plotting_widget.init_size() self.plotting_widget.init_size()
return super(WaveformDialog, self).resizeEvent(event) return super(WaveformDialog, self).resizeEvent(event)
def moveEvent(self, event):
"""
Get dpi, dpi_x, dpi_y whenever main window is moved
"""
screen = QtWidgets.QApplication.screenAt(event.pos())
if screen is not None:
curr_actual_dpi = (
screen.physicalDotsPerInch() * screen.devicePixelRatio())
self.dpi_x = screen.physicalDotsPerInchX()
self.dpi_y = screen.physicalDotsPerInchY()
self.actual_dpi = curr_actual_dpi
return super().moveEvent(event)
@QtCore.Slot() @QtCore.Slot()
def save_plot(self): def save_plot(self):
""" """
......
...@@ -153,7 +153,9 @@ class SelectChanelsToShowDialog(QDialog): ...@@ -153,7 +153,9 @@ class SelectChanelsToShowDialog(QDialog):
self.parent.start_tm, self.parent.start_tm,
self.parent.end_tm, self.parent.end_tm,
self.parent.time_ticks_total, self.parent.time_ticks_total,
new_pref_order) new_pref_order,
keep_zoom=True
)
self.parent.draw() self.parent.draw()
self.close() self.close()
...@@ -65,7 +65,7 @@ class TestGetChanPlotInfo(BaseTestCase): ...@@ -65,7 +65,7 @@ class TestGetChanPlotInfo(BaseTestCase):
with self.subTest("RT130 Seismic"): with self.subTest("RT130 Seismic"):
expected_result = {'param': 'Seismic data', expected_result = {'param': 'Seismic data',
'dbChannel': 'SEISMIC', 'dbChannel': 'SEISMIC',
'channel': 'DS2', 'channel': 'DS2-1',
'plotType': 'linesSRate', 'plotType': 'linesSRate',
'height': 8, 'height': 8,
'unit': '', 'unit': '',
...@@ -73,8 +73,8 @@ class TestGetChanPlotInfo(BaseTestCase): ...@@ -73,8 +73,8 @@ class TestGetChanPlotInfo(BaseTestCase):
'dbLabel': None, 'dbLabel': None,
'fixPoint': 0, 'fixPoint': 0,
'valueColors': '', 'valueColors': '',
'label': 'DS2'} 'label': 'DS2-1'}
self.assertDictEqual(get_chan_plot_info('DS2', 'RT130'), self.assertDictEqual(get_chan_plot_info('DS2-1', 'RT130'),
expected_result) expected_result)
with self.subTest("MSeed Seismic"): with self.subTest("MSeed Seismic"):
......
...@@ -156,6 +156,19 @@ class TestGetTPSForDiscontinuousData(BaseTestCase): ...@@ -156,6 +156,19 @@ class TestGetTPSForDiscontinuousData(BaseTestCase):
# last block of day0 has value # last block of day0 has value
self.assertIn(const.NUMBER_OF_5M_IN_DAY - 1, day0_indexes) self.assertIn(const.NUMBER_OF_5M_IN_DAY - 1, day0_indexes)
def test_overflow_data(self):
times = np.arange(self.start, self.end, 9*60) # 9m apart
# This data reproduce overflow data
data = np.random.randint(-10**6, 0, times.size, dtype='i4')
channel_data = {'tracesInfo': [{'times': times, 'data': data}]}
tps = get_tps_for_discontinuous_data(
channel_data, self.start_5mins_blocks)
self.assertEqual(len(tps), 2)
# Before adding dtype in np.square in get_tps_for_discontinuous_data,
# some tps data would be less than 0
lessthanzero_indexes = np.where(tps[0] < 0)[0]
self.assertEqual(lessthanzero_indexes.size, 0)
class TestGetTPSTimeByColorForADay(BaseTestCase): class TestGetTPSTimeByColorForADay(BaseTestCase):
def setUp(self): def setUp(self):
......