# -*- 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 adjust(self, event=None):
self.map._pan_start = None
self.map.end_pan()
[docs] def show(self):
self.update()
plt.show()