Source code for py_eddy_tracker.gui

# -*- coding: utf-8 -*-
"""
GUI class
"""

from datetime import datetime, timedelta
import logging

from matplotlib.projections import register_projection
import matplotlib.pyplot as plt
import numpy as np

from .generic import flatten_line_matrix, split_line

logger = logging.getLogger("pet")

try:
    from pylook.axes import PlatCarreAxes
except ImportError:
    from matplotlib.axes import Axes

[docs] class PlatCarreAxes(Axes): """ Class to replace missing pylook class """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.set_aspect("equal")
GUI_AXES = "full_axes"
[docs]class GUIAxes(PlatCarreAxes): """ Axes that uses full space available """ name = GUI_AXES
[docs] def end_pan(self, *args, **kwargs): (x0, x1), (y0, y1) = self.get_xlim(), self.get_ylim() x, y = (x1 + x0) / 2, (y1 + y0) / 2 dx, dy = (x1 - x0) / 2.0, (y1 - y0) / 2.0 r_coord = dx / dy # r_axe _, _, w_ax, h_ax = self.get_position(original=True).bounds w_fig, h_fig = self.figure.get_size_inches() r_ax = w_ax / h_ax * w_fig / h_fig if r_ax < r_coord: y0, y1 = y - dx / r_ax, y + dx / r_ax self.set_ylim(y0, y1) else: x0, x1 = x - dy * r_ax, x + dy * r_ax self.set_xlim(x0, x1) super().end_pan(*args, **kwargs)
register_projection(GUIAxes)
[docs]def no(*args, **kwargs): return False
[docs]class GUI: __slots__ = ( "datasets", "figure", "map", "time_ax", "param_ax", "settings", "d_indexs", "m", "last_event", ) COLORS = ("r", "g", "b", "y", "k") KEYTIME = dict(down=-1, up=1, pagedown=-5, pageup=5) def __init__(self, **datasets): self.datasets = datasets self.d_indexs = dict() self.m = dict() self.set_initial_values() self.setup() self.last_event = datetime.now() self.draw() self.event()
[docs] def set_initial_values(self): t0, t1 = 1e6, 0 for dataset in self.datasets.values(): t0_, t1_ = dataset.period t0, t1 = min(t0, t0_), max(t1, t1_) logger.debug("period detected %f -> %f", t0, t1) self.settings = dict(period=(t0, t1), now=t1)
@property def now(self): return self.settings["now"] @now.setter def now(self, value): self.settings["now"] = value @property def period(self): return self.settings["period"] @property def bbox(self): return self.map.get_xlim(), self.map.get_ylim() @bbox.setter def bbox(self, values): self.map.set_xlim(values[0], values[1]) self.map.set_ylim(values[2], values[3])
[docs] def indexs(self, dataset): (x0, x1), (y0, y1) = self.bbox x, y = dataset.longitude, dataset.latitude m = (x0 < x) & (x < x1) & (y0 < y) & (y < y1) & (self.now == dataset.time) return np.where(m)[0]
[docs] def med(self): self.map.set_xlim(-6, 37) self.map.set_ylim(30, 46)
[docs] def setup(self): self.figure = plt.figure() # map self.map = self.figure.add_axes((0, 0.25, 1, 0.75), projection=GUI_AXES) self.map.grid() self.map.tick_params("both", pad=-22) # self.map.tick_params("y", pad=-22) self.map.bg_cache = None # time ax self.time_ax = self.figure.add_axes((0, 0.15, 1, 0.1), facecolor=".95") self.time_ax.can_pan self.time_ax.set_xlim(*self.period) self.time_ax.press = False self.time_ax.can_pan = self.time_ax.can_zoom = no for i, dataset in enumerate(self.datasets.values()): self.time_ax.hist( dataset.time, bins=np.arange(self.period[0] - 0.5, self.period[1] + 0.51), color=self.COLORS[i], histtype="step", ) # param self.param_ax = self.figure.add_axes((0, 0, 1, 0.15), facecolor="0.2")
[docs] def hide_path(self, state): for name in self.datasets: self.m[name]["path_previous"].set_visible(state) self.m[name]["path_future"].set_visible(state)
[docs] def draw(self): self.m["mini_ax"] = self.figure.add_axes((0.3, 0.85, 0.4, 0.15), zorder=80) self.m["mini_ax"].grid() # map for i, (name, dataset) in enumerate(self.datasets.items()): kwargs = dict(color=self.COLORS[i]) self.m[name] = dict( contour_s=self.map.plot([], [], lw=1, label=name, **kwargs)[0], contour_e=self.map.plot([], [], lw=0.5, ls="-.", **kwargs)[0], path_previous=self.map.plot([], [], lw=0.5, **kwargs)[0], path_future=self.map.plot([], [], lw=0.2, ls=":", **kwargs)[0], mini_line=self.m["mini_ax"].plot([], [], **kwargs, lw=1)[0], ) # time_ax self.m["annotate"] = self.map.annotate( "", (0, 0), xycoords="figure pixels", zorder=100, fontsize=9, bbox=dict(boxstyle="round", facecolor="w", edgecolor="0.5", alpha=0.85), ) self.m["mini_ax"].set_visible(False) self.m["annotate"].set_visible(False) self.m["time_vline"] = self.time_ax.axvline(0, color="k", lw=1) self.m["time_text"] = self.time_ax.text( 0, 0, "", fontsize=8, bbox=dict(facecolor="w", alpha=0.75), verticalalignment="bottom", )
[docs] def update(self): time_text = [ (timedelta(days=int(self.now)) + datetime(1950, 1, 1)).strftime("%d/%m/%Y") ] # map xs, ys, ns = list(), list(), list() for j, (name, dataset) in enumerate(self.datasets.items()): i = self.indexs(dataset) self.d_indexs[name] = i self.m[name]["contour_s"].set_label(f"{name} {len(i)} eddies") if len(i) == 0: self.m[name]["contour_s"].set_data([], []) self.m[name]["contour_e"].set_data([], []) else: if "contour_lon_s" in dataset.elements: self.m[name]["contour_s"].set_data( flatten_line_matrix(dataset["contour_lon_s"][i]), flatten_line_matrix(dataset["contour_lat_s"][i]), ) if "contour_lon_e" in dataset.elements: self.m[name]["contour_e"].set_data( flatten_line_matrix(dataset["contour_lon_e"][i]), flatten_line_matrix(dataset["contour_lat_e"][i]), ) time_text.append(f"{i.shape[0]}") local_path = dataset.extract_ids(dataset["track"][i]) x, y, t, n, tr = ( local_path.longitude, local_path.latitude, local_path.time, local_path["n"], local_path["track"], ) m = t <= self.now if m.sum(): x_, y_ = split_line(x[m], y[m], tr[m]) self.m[name]["path_previous"].set_data(x_, y_) else: self.m[name]["path_previous"].set_data([], []) m = t >= self.now if m.sum(): x_, y_ = split_line(x[m], y[m], tr[m]) self.m[name]["path_future"].set_data(x_, y_) else: self.m[name]["path_future"].set_data([], []) m = t == self.now xs.append(x[m]), ys.append(y[m]), ns.append(n[m]) x, y, n = np.concatenate(xs), np.concatenate(ys), np.concatenate(ns) n_min = 0 if len(n) > 50: n_ = n.copy() n_.sort() n_min = n_[-50] for text in self.m.pop("texts", list()): text.remove() self.m["texts"] = [ self.map.text(x_, y_, n_) for x_, y_, n_ in zip(x, y, n) if n_ >= n_min ] self.map.legend() # time ax x, y = self.m["time_vline"].get_data() self.m["time_vline"].set_data(self.now, y) self.m["time_text"].set_text("\n".join(time_text)) self.m["time_text"].set_position((self.now, 0)) # force update self.map.figure.canvas.draw()
[docs] def event(self): self.figure.canvas.mpl_connect("resize_event", self.adjust) self.figure.canvas.mpl_connect("scroll_event", self.scroll) self.figure.canvas.mpl_connect("button_press_event", self.press) self.figure.canvas.mpl_connect("motion_notify_event", self.move) self.figure.canvas.mpl_connect("button_release_event", self.release) self.figure.canvas.mpl_connect("key_press_event", self.keyboard)
[docs] def keyboard(self, event): if event.key in self.KEYTIME: self.settings["now"] += self.KEYTIME[event.key] self.update() elif event.key == "home": self.settings["now"] = self.period[0] self.update() elif event.key == "end": self.settings["now"] = self.period[1] self.update()
[docs] def press(self, event): if event.inaxes == self.time_ax and self.m["time_vline"].contains(event)[0]: self.time_ax.press = True self.time_ax.bg_cache = self.figure.canvas.copy_from_bbox(self.time_ax.bbox)
[docs] def get_infos(self, name, index): i = self.d_indexs[name][index] d = self.datasets[name] now = d.obs[i] tr = now["track"] nb = d.nb_obs_by_track[tr] i_first = d.index_from_track[tr] track = d.obs[i_first : i_first + nb] nb -= 1 t0 = timedelta(days=track[0]["time"]) + datetime(1950, 1, 1) t1 = timedelta(days=track[-1]["time"]) + datetime(1950, 1, 1) txt = f"--{name}--\n" txt += f" {t0} -> {t1}\n" txt += f" Tracks : {tr} {now['n']}/{nb} ({now['n'] / nb * 100:.2f} %)\n" for label, n, f, u in ( ("Amp.", "amplitude", 100, "cm"), ("S. radius", "radius_s", 1e-3, "km"), ("E. radius", "radius_e", 1e-3, "km"), ): v = track[n] * f min_, max_, mean_, std_ = v.min(), v.max(), v.mean(), v.std() txt += f" {label} : {now[n] * f:.1f} {u} ({min_:.1f} <-{mean_:.1f}+-{std_:.1f}-> {max_:.1f})\n" return track, txt.strip()
[docs] def move(self, event): if event.inaxes == self.time_ax and self.time_ax.press: x, y = self.m["time_vline"].get_data() self.m["time_vline"].set_data(event.xdata, y) self.figure.canvas.restore_region(self.time_ax.bg_cache) self.time_ax.draw_artist(self.m["time_vline"]) self.figure.canvas.blit(self.time_ax.bbox) if event.inaxes == self.map: touch = dict() for name in self.datasets.keys(): flag, data = self.m[name]["contour_s"].contains(event) if flag: # 51 is for contour on 50 point must be rewrote touch[name] = data["ind"][0] // 51 a = self.m["annotate"] ax = self.m["mini_ax"] if touch: if not a.get_visible(): self.map.bg_cache = self.figure.canvas.copy_from_bbox(self.map.bbox) a.set_visible(True) ax.set_visible(True) else: self.figure.canvas.restore_region(self.map.bg_cache) a.set_x(event.x), a.set_y(event.y) txt = list() x0_, x1_, y1_ = list(), list(), list() for name in self.datasets.keys(): if name in touch: track, txt_ = self.get_infos(name, touch[name]) txt.append(txt_) x, y = track["time"], track["radius_s"] / 1e3 self.m[name]["mini_line"].set_data(x, y) x0_.append(x.min()), x1_.append(x.max()), y1_.append(y.max()) else: self.m[name]["mini_line"].set_data([], []) ax.set_xlim(min(x0_), max(x1_)), ax.set_ylim(0, max(y1_)) a.set_text("\n".join(txt)) self.map.draw_artist(a) self.map.draw_artist(ax) self.figure.canvas.blit(self.map.bbox) if not flag and self.map.bg_cache is not None and a.get_visible(): a.set_visible(False) ax.set_visible(False) self.figure.canvas.restore_region(self.map.bg_cache) self.map.draw_artist(a) self.map.draw_artist(ax) self.figure.canvas.blit(self.map.bbox) self.map.bg_cache = None
[docs] def release(self, event): if self.time_ax.press: self.time_ax.press = False self.settings["now"] = int(self.m["time_vline"].get_data()[0]) self.update()
[docs] def scroll(self, event): if event.inaxes != self.map: return if event.button == "up": self.settings["now"] += 1 if event.button == "down": self.settings["now"] -= 1 self.update()
[docs] def adjust(self, event=None): self.map._pan_start = None self.map.end_pan()
[docs] def show(self): self.update() plt.show()