# -*- coding: utf-8 -*-
"""
Entry point of graphic user interface
"""
from datetime import datetime, timedelta
from itertools import chain
import logging
from matplotlib import pyplot
from matplotlib.animation import FuncAnimation
from matplotlib.axes import Axes
from matplotlib.collections import LineCollection
from numpy import arange, where
from .. import EddyParser
from ..gui import GUI
from ..observations.tracking import TrackEddiesObservations
from ..poly import create_vertice
logger = logging.getLogger("pet")
[docs]class Anim:
def __init__(
self, eddy, intern=False, sleep_event=0.1, graphic_information=False, **kwargs
):
self.eddy = eddy
x_name, y_name = eddy.intern(intern)
self.t, self.x, self.y = eddy.time, eddy[x_name], eddy[y_name]
self.x_core, self.y_core, self.track = eddy["lon"], eddy["lat"], eddy["track"]
self.graphic_informations = graphic_information
self.pause = False
self.period = self.eddy.period
self.sleep_event = sleep_event
self.mappables = list()
self.field_color = None
self.field_txt = None
self.time_field = False
self.txt = None
self.ax = None
self.kw_label = dict()
self.setup(**kwargs)
[docs] def setup(
self,
cmap="jet",
lut=None,
field_color="time",
field_txt="track",
range_color=(None, None),
nb_step=25,
figsize=(8, 6),
position=(0.05, 0.05, 0.9, 0.9),
**kwargs,
):
self.kw_label["fontsize"] = kwargs.pop("fontsize", 12)
self.kw_label["fontweight"] = kwargs.pop("fontweight", "demibold")
# To text each visible eddy
if field_txt:
self.field_txt = self.eddy[field_txt]
if field_color:
# To color each visible eddy
self.field_color = self.eddy[field_color].astype("f4")
rg = range_color
if rg[0] is None and rg[1] is None and field_color == "time":
self.time_field = True
else:
rg = (
self.field_color.min() if rg[0] is None else rg[0],
self.field_color.max() if rg[1] is None else rg[1],
)
self.field_color = (self.field_color - rg[0]) / (rg[1] - rg[0])
self.colors = pyplot.get_cmap(cmap, lut=lut)
self.nb_step = nb_step
# plot
if "figure" in kwargs:
self.fig = kwargs.pop("figure")
else:
self.fig = pyplot.figure(figsize=figsize, **kwargs)
t0, t1 = self.period
self.fig.suptitle(f"{t0} -> {t1}")
if isinstance(position, Axes):
self.ax = position
else:
x_min, x_max = self.x_core.min() - 2, self.x_core.max() + 2
d_x = x_max - x_min
y_min, y_max = self.y_core.min() - 2, self.y_core.max() + 2
d_y = y_max - y_min
self.ax = self.fig.add_axes(position, projection="full_axes")
self.ax.set_xlim(x_min, x_max), self.ax.set_ylim(y_min, y_max)
self.ax.set_aspect("equal")
self.ax.grid()
self.txt = self.ax.text(
x_min + 0.05 * d_x, y_min + 0.05 * d_y, "", zorder=10
)
self.segs = list()
self.t_segs = list()
self.c_segs = list()
if field_color is None:
self.contour = LineCollection([], zorder=1, color="gray")
else:
self.contour = LineCollection([], zorder=1)
self.ax.add_collection(self.contour)
self.fig.canvas.draw()
self.fig.canvas.mpl_connect("key_press_event", self.keyboard)
self.fig.canvas.mpl_connect("resize_event", self.reset_bliting)
[docs] def reset_bliting(self, event):
self.contour.set_visible(False)
self.txt.set_visible(False)
for m in self.mappables:
m.set_visible(False)
self.fig.canvas.draw()
self.bg_cache = self.fig.canvas.copy_from_bbox(self.ax.bbox)
self.contour.set_visible(True)
self.txt.set_visible(True)
for m in self.mappables:
m.set_visible(True)
[docs] def show(self, infinity_loop=False):
pyplot.show(block=False)
# save background for future bliting
self.fig.canvas.draw()
self.bg_cache = self.fig.canvas.copy_from_bbox(self.ax.bbox)
loop = True
t0, t1 = self.period
while loop:
self.now = t0
while True:
dt = self.sleep_event
if not self.pause:
d0 = datetime.now()
self.next()
dt_draw = (datetime.now() - d0).total_seconds()
dt = self.sleep_event - dt_draw
if dt < 0:
# self.sleep_event = dt_draw * 1.01
dt = 1e-10
if dt == 0:
dt = 1e-10
self.fig.canvas.start_event_loop(dt)
if self.now > t1:
break
if infinity_loop:
self.fig.canvas.start_event_loop(0.5)
else:
loop = False
[docs] def next(self):
self.now += 1
return self.draw_contour()
[docs] def prev(self):
self.now -= 1
return self.draw_contour()
[docs] def func_animation(self, frame):
while self.mappables:
self.mappables.pop().remove()
self.now = frame
self.update()
artists = [self.contour, self.txt]
artists.extend(self.mappables)
return artists
[docs] def update(self):
m = self.t == self.now
color = self.field_color is not None
if m.sum():
segs = list()
t = list()
c = list()
for i in where(m)[0]:
segs.append(create_vertice(self.x[i], self.y[i]))
if color:
c.append(self.field_color[i])
t.append(self.now)
self.segs.append(segs)
if color:
self.c_segs.append(c)
self.t_segs.append(t)
self.contour.set_paths(chain(*self.segs))
if color:
if self.time_field:
self.contour.set_color(
self.colors(
[
(self.nb_step - self.now + i) / self.nb_step
for i in chain(*self.c_segs)
]
)
)
else:
self.contour.set_color(self.colors(list(chain(*self.c_segs))))
# linewidth will be link to time delay
self.contour.set_lw(
[
(1 - (self.now - i) / self.nb_step) * 2.5 if i <= self.now else 0
for i in chain(*self.t_segs)
]
)
# Update date txt and info
if self.txt is not None:
txt = f"{(timedelta(int(self.now)) + datetime(1950,1,1)).strftime('%Y/%m/%d')}"
if self.graphic_informations:
txt += f"- {1/self.sleep_event:.0f} frame/s"
self.txt.set_text(txt)
self.ax.draw_artist(self.txt)
# Update id txt
if self.field_txt is not None:
for i in where(m)[0]:
mappable = self.ax.text(
self.x_core[i], self.y_core[i], self.field_txt[i], **self.kw_label
)
self.mappables.append(mappable)
self.ax.draw_artist(mappable)
self.ax.draw_artist(self.contour)
# Remove first segment to keep only T contour
if len(self.segs) > self.nb_step:
self.segs.pop(0)
self.t_segs.pop(0)
if color:
self.c_segs.pop(0)
[docs] def draw_contour(self):
# select contour for this time step
while self.mappables:
self.mappables.pop().remove()
self.ax.figure.canvas.restore_region(self.bg_cache)
self.update()
# paint updated artist
self.ax.figure.canvas.blit(self.ax.bbox)
[docs] def keyboard(self, event):
if event.key == "escape":
exit()
elif event.key == " ":
self.pause = not self.pause
elif event.key == "+":
self.sleep_event *= 0.9
elif event.key == "-":
self.sleep_event *= 1.1
elif event.key == "right" and self.pause:
self.next()
elif event.key == "left" and self.pause:
# we remove 2 step to add 1 so we rewind of only one
self.segs.pop(-1)
self.segs.pop(-1)
self.t_segs.pop(-1)
self.t_segs.pop(-1)
self.c_segs.pop(-1)
self.c_segs.pop(-1)
self.prev()
[docs]def anim():
parser = EddyParser(
"""Anim eddy, keyboard shortcut : Escape => exit, SpaceBar => pause,
left arrow => t - 1, right arrow => t + 1, + => speed increase of 10 %, - => speed decrease of 10 %"""
)
parser.add_argument("filename", help="eddy atlas")
parser.add_argument("id", help="Track id to anim", type=int, nargs="*")
parser.contour_intern_arg()
parser.add_argument(
"--keep_step", default=25, help="number maximal of step displayed", type=int
)
parser.add_argument("--cmap", help="matplotlib colormap used")
parser.add_argument("--all", help="All eddies will be drawed", action="store_true")
parser.add_argument(
"--time_sleep",
type=float,
default=0.01,
help="Sleeping time in second between 2 frame",
)
parser.add_argument(
"--infinity_loop", action="store_true", help="Press Escape key to stop loop"
)
parser.add_argument(
"--first_centered",
action="store_true",
help="Longitude will be centered on first obs.",
)
parser.add_argument(
"--field", default="time", help="Field use to color contour instead of time"
)
parser.add_argument("--txt_field", default="track", help="Field use to text eddy")
parser.add_argument(
"--vmin", default=None, type=float, help="Inferior bound to color contour"
)
parser.add_argument(
"--vmax", default=None, type=float, help="Upper bound to color contour"
)
parser.add_argument("--mp4", help="Filename to save animation (mp4)")
args = parser.parse_args()
variables = list(
set(["time", "track", "longitude", "latitude", args.field, args.txt_field])
)
variables.extend(TrackEddiesObservations.intern(args.intern, public_label=True))
eddies = TrackEddiesObservations.load_file(
args.filename, include_vars=set(variables)
)
if not args.all:
if len(args.id) == 0:
raise Exception(
"You need to specify id to display or ask explicity all with --all option"
)
eddies = eddies.extract_ids(args.id)
if args.first_centered:
# TODO: include to observation class
x0 = eddies.lon[0]
eddies.lon[:] = (eddies.lon - x0 + 180) % 360 + x0 - 180
eddies.contour_lon_e[:] = (
(eddies.contour_lon_e.T - eddies.lon + 180) % 360 + eddies.lon - 180
).T
kw = dict()
if args.mp4:
kw["figsize"] = (16, 9)
kw["dpi"] = 120
a = Anim(
eddies,
intern=args.intern,
sleep_event=args.time_sleep,
cmap=args.cmap,
nb_step=args.keep_step,
field_color=args.field,
field_txt=args.txt_field,
range_color=(args.vmin, args.vmax),
graphic_information=logger.getEffectiveLevel() == logging.DEBUG,
**kw,
)
if args.mp4 is None:
a.show(infinity_loop=args.infinity_loop)
else:
kwargs = dict(frames=arange(*a.period), interval=50)
ani = FuncAnimation(a.fig, a.func_animation, **kwargs)
ani.save(args.mp4, fps=30, extra_args=["-vcodec", "libx264"])
[docs]def gui_parser():
parser = EddyParser("Eddy atlas GUI")
parser.add_argument("atlas", nargs="+")
parser.add_argument("--med", action="store_true")
parser.add_argument("--nopath", action="store_true", help="Don't draw path")
return parser.parse_args()
[docs]def guieddy():
args = gui_parser()
atlas = {
dataset: TrackEddiesObservations.load_file(dataset) for dataset in args.atlas
}
g = GUI(**atlas)
if args.med:
g.med()
g.hide_path(not args.nopath)
g.show()