"""Main GUI window for the HLMA application.
Handles plotting, visualization, and analysis of lightning data,
including animations and interactive polygon-based selections.
Connects user actions to underlying data processing and the FLASH
algorithm, serving as the primary entry point for the application.
Attributes
----------
settings : QSettings
Stores application-specific settings.
state : State
Application state containing loaded data and plot configuration.
anim : Animate
Animation controller for temporal lightning visualizations.
ui : SimpleNamespace
Container for all UI widgets.
polyfilter : PolygonFilter
Tool for interactive polygon-based selection.
"""
import logging
import pickle
import sys
import time
import webbrowser
from datetime import datetime
from pathlib import Path
import numpy as np
from matplotlib import colors as mcolors
from pandas import date_range
from PyQt6.QtCore import QSettings
from PyQt6.QtGui import QIcon, QKeySequence, QShortcut
from PyQt6.QtWidgets import QApplication, QFileDialog, QMainWindow
from bts import dot_to_dot, mc_caul, open_entln, open_lylout
from polygon import PolygonFilter
from setup import (
Animate,
LoadingDialog,
State,
connect_ui,
setup_folders,
setup_ui,
setup_utility,
)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S")
logger = logging.getLogger("hlma.py")
[docs]
class HLMA(QMainWindow):
"""Main GUI window for the HLMA application.
Handles plotting, visualization, and analysis of lightning data,
including animations and interactive polygon-based selections.
Connects user actions to underlying data processing and the FLASH
algorithm, serving as the primary entry point for the application.
Attributes
----------
settings : QSettings
Stores application-specific settings.
state : State
Application state containing loaded data and plot configuration.
anim : Animate
Animation controller for temporal lightning visualizations.
ui : SimpleNamespace
Container for all UI widgets.
polyfilter : PolygonFilter
Tool for interactive polygon-based selection.
"""
def __init__(self) -> None:
"""Initialize the HLMA main GUI application window.
Sets up application folders, utility data, UI components, and
connections. Initializes the animation controller, state object,
and polygon selection tool. Also sets up undo/redo shortcuts.
Parameters
----------
None
Returns
-------
None
"""
super().__init__()
self.settings = QSettings("HLMA", "LAt")
# setting up
# state
self.state = State()
self.state.__dict__["replot"] = self.visplot
self.anim = Animate()
self.anim.timer.connect(self._animate_step)
# folders
setup_folders()
# utiilty
self.util = setup_utility()
# ui
self.ui = setup_ui(self)
# Polygonning tool
self.polyfilter = PolygonFilter(self)
# connections
connect_ui(self, self.ui)
# undo and redo
undo_shortcut = QShortcut(QKeySequence.StandardKey.Undo, self)
undo_shortcut.activated.connect(self.undo)
redo_shortcut = QShortcut(QKeySequence.StandardKey.Redo, self)
redo_shortcut.activated.connect(self.redo)
# go!
self.ui.view_widget.setFocus()
logger.info("Application running.")
self.showMaximized()
[docs]
def import_lylout(self) -> None:
"""Open and load one or more LYLOUT files selected by the user.
Opens files using a file dialog, loads data into the state object,
updates time range and station statistics, and applies current filters.
Parameters
----------
None
Returns
-------
None
"""
files, _ = QFileDialog.getOpenFileNames(self, "Select LYLOUT files", self.settings.value("lylout_folder", ""), "Dat files (*.dat *.dat.gz)")
if files:
dialog = LoadingDialog("Opening selected LYLOUT files...")
dialog.show()
QApplication.processEvents()
self.settings.setValue("lylout_folder", str(Path(files[0]).parent))
# following syntax ensures one call to set_attr to appropriately track history
self.state.__dict__["all"], self.state.__dict__["stations"] = open_lylout(files)
logger.info("All LYLOUT files opened.")
self.ui.timemin.setText(self.state.all["datetime"].min().strftime("%Y-%m-%d %H:%M:%S"))
self.ui.timemax.setText(self.state.all["datetime"].max().strftime("%Y-%m-%d %H:%M:%S"))
self.ui.stats.set_data(self.state.stations, face_color=None, size=5, edge_width=1, edge_color="red", symbol="square")
self.filter()
dialog.close()
[docs]
def import_entln(self) -> None:
"""Open and load one or more ENTLN files, updating lightning strike data.
Loads ENTLN files selected by the user, updates the ground strike
dataset (`self.state.gsd`), assigns colors and symbols for CG and CC
strikes, and refreshes the UI plots.
Parameters
----------
None
Returns
-------
None
"""
if not len(self.state.all) > 0:
logger.warning("No LYLOUT data, cannot plot ENTLN")
return
files, _ = QFileDialog.getOpenFileNames(self, "Select ENTLN files", self.settings.value("entln_folder", ""), "CSV files (*.csv)")
if files:
dialog = LoadingDialog("Opening selected ENTLN files...")
dialog.show()
QApplication.processEvents()
self.settings.setValue("entln_folder", str(Path(files[0]).parent))
# See import_lylout for syntactic reasoning
temp = open_entln(files, self.state.all["datetime"].min())
self.state.gsd = temp
colors = [(1,0,0,1) if pc >= 0 else (0,0,1,1) for pc in temp["peakcurrent"]]
self.state.__dict__["gsd"]["colors"] = colors
logger.info("All ENTLN files opened.")
dialog.close()
if not self.state.gsd.empty:
# CG strikes
self.ui.v0.add(self.ui.gs0)
self.ui.v1.add(self.ui.gs1)
self.ui.v3.add(self.ui.gs3)
self.ui.v4.add(self.ui.gs4)
# CC strikes
self.ui.v0.add(self.ui.cc0)
self.ui.v1.add(self.ui.cc1)
self.ui.v3.add(self.ui.cc3)
self.ui.v4.add(self.ui.cc4)
self.state.gsd["symbol"] = ["triangle_up" if (val == 0) or (val == 40) else "x" for val in temp["type"]]
gs_data = temp[(temp["type"] == 0) | (temp["type"] == 40)]
cc_data = temp[temp["type"] == 1]
# Statements were becoming too long
if not gs_data.empty:
self.ui.gs0.set_data(
pos=np.column_stack([gs_data["utc_sec"].to_numpy(dtype=np.float32), gs_data["alt"].to_numpy(dtype=np.float32)]),
face_color=gs_data["colors"].to_list(),
edge_color=gs_data["colors"].to_list(),
size=5,
symbol=gs_data["symbol"],
)
if not cc_data.empty:
self.ui.cc0.set_data(
pos=np.column_stack([cc_data["utc_sec"].to_numpy(dtype=np.float32), cc_data["alt"].to_numpy(dtype=np.float32)]),
face_color=cc_data["colors"].to_list(),
edge_color=cc_data["colors"].to_list(),
size=5,
symbol=cc_data["symbol"],
)
coords = [["lon", "alt"], ["lon", "lat"], ["alt", "lat"]]
targets = [self.ui.gs1, self.ui.gs3, self.ui.gs4]
cc_targets = [self.ui.cc1, self.ui.cc3, self.ui.cc4]
# Same logic as before, just condensed to a loop
for (x, y), gs_plot, cc_plot in zip(coords, targets, cc_targets, strict=True):
gs_data = temp[(temp["type"] == 0) | (temp["type"] == 40)]
cc_data = temp[temp["type"] == 1]
if not gs_data.empty:
gs_plot.set_data(
pos=gs_data[[x, y]].to_numpy(dtype=np.float32),
face_color=gs_data["colors"].to_list(),
edge_color=gs_data["colors"].to_list(),
size=5,
symbol=gs_data["symbol"],
)
if not cc_data.empty:
cc_plot.set_data(
pos=cc_data[[x, y]].to_numpy(dtype=np.float32),
face_color=cc_data["colors"].to_list(),
edge_color=cc_data["colors"].to_list(),
size=5,
symbol=cc_data["symbol"],
)
# Induce change for state saving
self.state.__dict__["gsd_mask"] = np.ones([temp.shape[0]], dtype=bool)
[docs]
def import_state(self) -> None:
"""Load a previously saved application state from 'state/state.pkl'.
Reads a pickled state file and updates the internal `State` object
including datasets, plot options, and stations.
Parameters
----------
None
Returns
-------
None
"""
try:
with Path.open("state/state.pkl", "rb") as file:
save_state = pickle.load(file)
self.state.update(all = save_state["all"], stations = self.state.stations, plot = save_state["plot"], plot_options = save_state["plot_options"])
logger.info("Loaded state in state/state.pkl.")
except Exception as e:
logger.warning("Could not load state/state.pkl due to %s.", e)
[docs]
def export_dat(self) -> None:
"""Export selected LYLOUT data to 10-minute interval .dat files.
Groups the currently plotted data into 10-minute bins and writes
formatted .dat files to the output folder with relevant metadata.
Parameters
----------
None
Returns
-------
None
"""
temp = self.state.all[self.state.plot]
start = temp["datetime"].min().floor("10min")
end = temp["datetime"].max().ceil("10min")
bins = date_range(start, end, freq="10min", inclusive="left")
for i, chunk in enumerate(bins):
filename = f"LYLOUT_{datetime.strftime(chunk, '%y%m%d_%H%M%S')}_0600"
start = chunk
end = bins[i+1]
df_chunk = temp[(temp["datetime"] >= start) & (temp["datetime"] < end)]
df_chunk = df_chunk[["utc_sec", "lat", "lon", "alt", "chi", "number_stations", "pdb", "mask"]]
beginning_stuff = f"""Houston A&M Lightning Mapping System -- Selected Data
When exported: {datetime.now().ctime()}
Original data file: {Path.home()}
Data start time: {start}
Location: LYLOUT
Data: time (UT sec of day), lat, lon, alt(m), reduced chi^2, # of stations contributed, P(dBW), mask
Data format: f15.9 f11.6 f11.6 f8.1 f6.2 2i e11.4 4x
Number of events: {len(df_chunk)}
Flash stats: not saved
***data***\n"""
with Path.open(f"./output/{filename}.exported.dat", "w", newline="") as file:
file.write(beginning_stuff)
for _, row in df_chunk.iterrows():
line = (
f"{row.utc_sec:15.9f} "
f"{row.lat:11.6f} "
f"{row.lon:11.6f} "
f"{row.alt:8.1f} "
f"{row.chi:6.2f} "
f"{int(row.number_stations):2d} "
f"{row.pdb:11.4e} "
f"{int(row['mask'], 16):04x}\n"
)
file.write(line)
logger.info("Saved files in output.")
[docs]
def export_parquet(self) -> None:
"""Export the currently plotted LYLOUT data to a Parquet file.
Writes the plotted dataset to 'output/lylout.parquet' for efficient
storage and later use.
Parameters
----------
None
Returns
-------
None
"""
try:
if not self.state.all.empty:
self.state.all[self.state.plot].to_parquet("output/lylout.parquet", index=False)
logger.info("Saved file in output/lylout.parquet.")
except Exception as e:
logger.warning("Could not save file in output/lylout.parquet due to %s.", e)
[docs]
def export_state(self) -> None:
"""Save the current application state to 'state/state.pkl'.
Serializes the state attributes including datasets, plot options,
and stations, enabling later restoration.
Parameters
----------
None
Returns
-------
None
"""
try:
with Path.open("state/state.pkl", "wb") as file:
save_state = {"all": self.state.all, "stations": self.state.stations, "plot": self.state.plot, "plot_options": self.state.plot_options }
pickle.dump(save_state, file)
logger.info("Saved state in state/state.pkl.")
except Exception as e:
logger.warning("Could not save state in state/state.pkl due to %s", e)
[docs]
def export_image(self) -> None:
"""Capture the current view widget and save it as a PDF image.
Grabs the content of the main view widget and writes it to
'output/image.pdf'.
Parameters
----------
None
Returns
-------
None
"""
try:
pixmap = self.ui.view_widget.grab()
pixmap.save("output/image.pdf")
logger.info("Saved image in output/image.pdf")
except Exception as e:
logger.warning("Could not save image in output/image.pdf due to %s", e)
[docs]
def options_clear(self) -> None:
"""Clear all plotted data from plots and histograms.
Resets the scatter plots (`s0`, `s1`, `s3`, `s4`) and histogram (`hist`)
in the UI to empty arrays, effectively clearing visualizations.
Parameters
----------
None
Returns
-------
None
"""
self.ui.s0.set_data(np.empty((0, 2)))
self.ui.s1.set_data(np.empty((0, 2)))
self.ui.s3.set_data(np.empty((0, 2)))
self.ui.s4.set_data(np.empty((0, 2)))
self.ui.hist.set_data(np.empty((0, 2)))
[docs]
def options_reset(self) -> None:
"""Reset the camera views of all main visualization widgets.
Restores the default orientation and zoom for the cameras of view
widgets `v0` through `v4`.
Parameters
----------
None
Returns
-------
None
"""
self.ui.v0.camera.reset()
self.ui.v1.camera.reset()
self.ui.v2.camera.reset()
self.ui.v3.camera.reset()
self.ui.v4.camera.reset()
[docs]
def flash_dtd(self) -> None:
"""Run the dot-to-dot (DTD) flash detection algorithm.
Executes the `dot_to_dot` function on the current state, updating
flash detection results while showing a loading dialog.
Parameters
----------
None
Returns
-------
None
"""
dialog = LoadingDialog("Running dot to dot flash algorithm..")
dialog.show()
QApplication.processEvents()
dot_to_dot(self.state)
dialog.close()
[docs]
def flash_mccaul(self) -> None:
"""Run the McCaul flash detection algorithm.
Executes the `mc_caul` function on the current state, updating
flash detection results while showing a loading dialog.
Parameters
----------
None
Returns
-------
None
"""
dialog = LoadingDialog("Running McCaul flash algorithm..")
dialog.show()
QApplication.processEvents()
mc_caul(self.state)
dialog.close()
[docs]
def help_about(self) -> None:
"""Open the HLMA project About webpage.
Launches the HLMA About page in the default web browser.
Parameters
----------
None
Returns
-------
None
"""
webbrowser.open("https://lightning.tamu.edu/hlma/")
[docs]
def help_color(self) -> None:
"""Open the Colorcet colormap user guide.
Launches the Colorcet documentation for linear sequential colormaps.
Parameters
----------
None
Returns
-------
None
"""
webbrowser.open("https://colorcet.holoviz.org/user_guide/Continuous.html#linear-sequential-colormaps-for-plotting-magnitudes")
[docs]
def filter(self) -> None:
"""Apply user-defined filters to the loaded lightning data.
Reads filter criteria from UI elements, evaluates the query against
`self.state.all`, updates `self.state.plot`, resets polygon selection
mask, and triggers a replot.
Parameters
----------
None
Returns
-------
None
"""
if self.state.all is None:
return
query = (
f"(datetime >= '{self.ui.timemin.text()}') & (datetime <= '{self.ui.timemax.text()}') & "
f"(alt >= {float(self.ui.altmin.text()) * 1000}) & (alt <= {float(self.ui.altmax.text()) * 1000}) & "
f"(chi >= {self.ui.chimin.text()}) & (chi <= {self.ui.chimax.text()}) & "
f"(pdb >= {self.ui.powermin.text()}) & (pdb <= {self.ui.powermax.text()}) & "
f"(number_stations >= {self.ui.stationsmin.text()})"
)
self.state.__dict__["plot"] = self.state.all.eval(query)
self.state.replot()
[docs]
def animate(self) -> None:
"""Start animating the currently loaded lightning data.
Initializes animation timing, sets the active flag, and starts the
animation timer.
Parameters
----------
None
Returns
-------
None
"""
if self.state.all.empty:
logger.info("No data to animate.")
return
logger.info("Starting animation.")
self.anim.start_time = time.perf_counter()
self.anim.active = True
self.anim.timer.start()
def _animate_step(self, _: object) -> None:
"""Perform a single step of the lightning data animation.
Updates visualization plots incrementally based on elapsed time and
stops the animation when complete.
Parameters
----------
_ : object
Timer event passed by the animation timer.
Returns
-------
None
"""
if not self.anim.active:
return
n = np.count_nonzero(self.state.plot)
elapsed = time.perf_counter() - self.anim.start_time
progress = min(1.0, elapsed / self.anim.duration)
n_vis = int(progress * n)
# FIXME: should find a way to do this without having to recalculate this every time
temp = self.state.all[self.state.plot].sort_values(by=self.anim.var).iloc[:n_vis]
temp.alt /= 1000
cvar = self.state.plot_options.cvar
cmap = self.state.plot_options.cmap
arr = temp[cvar].to_numpy()
norm = (arr - arr.min()) / (arr.max() - arr.min())
colors = cmap(norm)
positions = np.column_stack([temp["seconds"].to_numpy(dtype=np.float32),temp["alt"].to_numpy(dtype=np.float32)])
self.ui.s0.set_data(pos=positions, face_color=colors, size=1, edge_width=0)
positions = temp[["lon", "alt"]].to_numpy().astype(np.float32)
self.ui.s1.set_data(pos=positions, face_color=colors, size=1, edge_width=0)
bins = 200
counts, edges = np.histogram(temp["alt"], bins=bins)
centers = (edges[:-1] + edges[1:]) / 2
line_data = np.column_stack([counts, centers])
self.ui.hist.set_data(pos=line_data, color=(1, 1, 1, 1), width=1)
positions = temp[["lon", "lat"]].to_numpy().astype(np.float32)
self.ui.s3.set_data(pos=positions, face_color=colors, size=1, edge_width=0)
positions = temp[["alt", "lat"]].to_numpy().astype(np.float32)
self.ui.s4.set_data(pos=positions, face_color=colors, size=1, edge_width=0)
if progress >= 1.0:
logger.info("Finished animation.")
self.anim.timer.stop()
self.anim.active = False
[docs]
def visplot(self) -> None:
"""Update all visualization plots with the current lightning and map data.
Refreshes scatter plots (`s0`, `s1`, `s3`, `s4`), histogram (`hist`),
and map layers using the currently selected data (`self.state.plot`) and
map features (`self.state.plot_options.map` and `features`). Handles
coloring based on the selected variable and adds or removes CG and CC
flash visuals depending on data availability.
Parameters
----------
None
Returns
-------
None
"""
logger.info("Starting vis.py plotting.")
temp = self.state.all[self.state.plot]
temp.alt /= 1000
cvar = self.state.plot_options.cvar
cmap = self.state.plot_options.cmap
arr = temp[cvar].to_numpy()
norm = (arr - arr.min()) / (arr.max() - arr.min())
colors = cmap(norm)
positions = np.column_stack([temp["seconds"].to_numpy(dtype=np.float32),temp["alt"].to_numpy(dtype=np.float32)])
self.ui.s0.set_data(pos=positions, face_color=colors, size=1, edge_width=0)
self.ui.v0.camera.set_range(x=(positions[:,0].min(), positions[:,0].max()), y=(0, 20))
self.ui.v0.camera.set_default_state()
positions = temp[["lon", "alt"]].to_numpy().astype(np.float32)
self.ui.s1.set_data(pos=positions, face_color=colors, size=1, edge_width=0)
self.ui.v1.camera.set_range(x=(positions[:,0].min(), positions[:,0].max()), y=(0, positions[:,1].max()))
self.ui.v1.camera.set_default_state()
bins = 200
counts, edges = np.histogram(temp["alt"], bins=bins)
centers = (edges[:-1] + edges[1:]) / 2
line_data = np.column_stack([counts, centers])
self.ui.hist.set_data(pos=line_data, color=(1, 1, 1, 1), width=1)
self.ui.v2.camera.set_range(x=(0, counts.max()), y=(0, 20))
self.ui.v2.camera.set_default_state()
positions_list = []
colors_list = []
minx, maxx = temp.lon.min(), temp.lon.max()
miny, maxy = temp.lat.min(), temp.lat.max()
map_gdf = self.state.plot_options.map.cx[minx:maxx, miny:maxy]
if not map_gdf.empty:
pos_map = np.vstack(map_gdf.geometry.boundary.explode(index_parts=False).apply(lambda p: np.append(np.array(p.coords, np.float32),np.array([[np.nan, np.nan]]),axis=0)).values)
color_map = np.tile(np.array([1, 1, 1, 1.0], dtype=np.float32), (pos_map.shape[0], 1))
positions_list.append(pos_map)
colors_list.append(color_map)
for fdict in self.state.plot_options.features.values():
gdf = fdict["gdf"].cx[minx:maxx, miny:maxy]
if gdf.empty:
continue
pos_array = np.vstack(gdf.geometry.explode(index_parts=False).apply(lambda p: np.append(np.array(p.coords, np.float32),np.array([[np.nan, np.nan]]), axis=0)).values)
colors_array = np.tile(np.array(mcolors.to_rgba(fdict["color"]), dtype=np.float32), (pos_array.shape[0], 1))
positions_list.append(pos_array)
colors_list.append(colors_array)
if positions_list:
map_positions = np.vstack(positions_list)
map_colors = np.vstack(colors_list)
else:
map_positions = np.empty((0, 2), dtype=np.float32)
map_colors = np.empty((0, 4), dtype=np.float32)
self.ui.map.set_data(pos=map_positions, color=map_colors)
positions = temp[["lon", "lat"]].to_numpy().astype(np.float32)
self.ui.s3.set_data(pos=positions, face_color=colors, size=1, edge_width=0)
self.ui.v3.camera.set_range(x=(positions[:,0].min(), positions[:,0].max()), y=(positions[:,1].min(), positions[:,1].max()))
self.ui.v3.camera.set_default_state()
positions = temp[["alt", "lat"]].to_numpy().astype(np.float32)
self.ui.s4.set_data(pos=positions, face_color=colors, size=1, edge_width=0)
self.ui.v4.camera.set_range(x=(0, positions[:,0].max()), y=(positions[:,1].min(), positions[:,1].max()))
self.ui.v4.camera.set_default_state()
temp = self.state.gsd[self.state.gsd_mask]
if self.state.gsd.empty:
gs_empty = True
cc_empty = True
else:
gs_mask = self.state.gsd["type"].isin([0, 40])
cc_mask = self.state.gsd["type"] == 1
gs_empty = len(self.state.gsd[gs_mask & self.state.gsd_mask]) == 0
cc_empty = len(self.state.gsd[cc_mask & self.state.gsd_mask]) == 0
# Handle when only gs data is empty
if gs_empty and not cc_empty:
# Remove only gs visuals
if self.ui.gs0 in self.ui.v0.scene.children:
self.ui.v0.scene.children.remove(self.ui.gs0)
self.ui.gs0.parent = None
if self.ui.gs1 in self.ui.v1.scene.children:
self.ui.v1.scene.children.remove(self.ui.gs1)
self.ui.gs1.parent = None
if self.ui.gs3 in self.ui.v3.scene.children:
self.ui.v3.scene.children.remove(self.ui.gs3)
self.ui.gs3.parent = None
if self.ui.gs4 in self.ui.v4.scene.children:
self.ui.v4.scene.children.remove(self.ui.gs4)
self.ui.gs4.parent = None
# Handle when only cc data is empty
elif cc_empty and not gs_empty:
# Remove only cc visuals
if self.ui.cc0 in self.ui.v0.scene.children:
self.ui.v0.scene.children.remove(self.ui.cc0)
self.ui.cc0.parent = None
if self.ui.cc1 in self.ui.v1.scene.children:
self.ui.v1.scene.children.remove(self.ui.cc1)
self.ui.cc1.parent = None
if self.ui.cc3 in self.ui.v3.scene.children:
self.ui.v3.scene.children.remove(self.ui.cc3)
self.ui.cc3.parent = None
if self.ui.cc4 in self.ui.v4.scene.children:
self.ui.v4.scene.children.remove(self.ui.cc4)
self.ui.cc4.parent = None
# Handle when both are empty
elif gs_empty and cc_empty:
# Remove both gs and cc visuals
targets = [
(self.ui.v0, self.ui.gs0, self.ui.cc0),
(self.ui.v1, self.ui.gs1, self.ui.cc1),
(self.ui.v3, self.ui.gs3, self.ui.cc3),
(self.ui.v4, self.ui.gs4, self.ui.cc4),
]
for v, gs, cc in targets:
if gs in v.scene.children:
v.scene.children.remove(gs)
gs.parent = None
if cc in v.scene.children:
v.scene.children.remove(cc)
cc.parent = None
# Both are not empty
else:
if self.ui.gs0 not in self.ui.v0.scene.children:
self.ui.v0.add(self.ui.gs0)
if self.ui.gs1 not in self.ui.v1.scene.children:
self.ui.v1.add(self.ui.gs1)
if self.ui.gs3 not in self.ui.v3.scene.children:
self.ui.v3.add(self.ui.gs3)
if self.ui.gs4 not in self.ui.v4.scene.children:
self.ui.v4.add(self.ui.gs4)
if self.ui.cc0 not in self.ui.v0.scene.children:
self.ui.v0.add(self.ui.cc0)
if self.ui.cc1 not in self.ui.v1.scene.children:
self.ui.v1.add(self.ui.cc1)
if self.ui.cc3 not in self.ui.v3.scene.children:
self.ui.v3.add(self.ui.cc3)
if self.ui.cc4 not in self.ui.v4.scene.children:
self.ui.v4.add(self.ui.cc4)
gs_data = temp[(temp["type"] == 0) | (temp["type"] == 40)]
cc_data = temp[temp["type"] == 1]
# Statements were becoming too long
if not gs_data.empty:
self.ui.gs0.set_data(
pos=np.column_stack([gs_data["utc_sec"].to_numpy(dtype=np.float32), gs_data["alt"].to_numpy(dtype=np.float32)]),
face_color=gs_data["colors"].to_list(),
edge_color=gs_data["colors"].to_list(),
size=5,
symbol=gs_data["symbol"],
)
if not cc_data.empty:
self.ui.cc0.set_data(
pos=np.column_stack([cc_data["utc_sec"].to_numpy(dtype=np.float32), cc_data["alt"].to_numpy(dtype=np.float32)]),
face_color=cc_data["colors"].to_list(),
edge_color=cc_data["colors"].to_list(),
size=5,
symbol=cc_data["symbol"],
)
coords = [["lon", "alt"], ["lon", "lat"], ["alt", "lat"]]
targets = [self.ui.gs1, self.ui.gs3, self.ui.gs4]
cc_targets = [self.ui.cc1, self.ui.cc3, self.ui.cc4]
# Same logic as before, just condensed to a loop
for (x, y), gs_plot, cc_plot in zip(coords, targets, cc_targets, strict=True):
gs_data = temp[(temp["type"] == 0) | (temp["type"] == 40)]
cc_data = temp[temp["type"] == 1]
if not gs_data.empty:
gs_plot.set_data(
pos=gs_data[[x, y]].to_numpy(dtype=np.float32),
face_color=gs_data["colors"].to_list(),
edge_color=gs_data["colors"].to_list(),
size=5,
symbol=gs_data["symbol"],
)
if not cc_data.empty:
cc_plot.set_data(
pos=cc_data[[x, y]].to_numpy(dtype=np.float32),
face_color=cc_data["colors"].to_list(),
edge_color=cc_data["colors"].to_list(),
size=5,
symbol=cc_data["symbol"],
)
logger.info("Finished vis.py plotting.")
[docs]
def undo(self) -> None:
"""Revert the last user action or selection.
If polygon selections exist in `self.polyfilter`, removes the last
polygon. Otherwise, restores the previous application state from
`self.state.history` and updates `self.state.future` to allow redo.
Refreshes visualizations via `self.state.replot()`.
Parameters
----------
None
Returns
-------
None
"""
logger.info("Undo called")
if len(self.polyfilter.clicks) > 0:
self.polyfilter.clicks.pop()
self.polyfilter.handle_poly_plot()
elif self.state.history:
self.state.future.append(self.state)
self.state = self.state.history.pop()
self.state.replot()
[docs]
def redo(self) -> None:
"""Reapply an action that was previously undone.
Restores the most recent future state from `self.state.future`, moves
the current state to `self.state.history`, and updates visualizations
by calling `self.state.replot()`.
Parameters
----------
None
Returns
-------
None
"""
logger.info("Redo called")
if self.state.future:
self.state.history.append(self.state)
self.state = self.state.future.pop()
self.state.replot()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = HLMA()
window.setWindowTitle("Aggie XLMA")
window.setWindowIcon(QIcon("assets/icons/hlma.svg"))
window.show()
sys.exit(app.exec())