Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/testMaps.yml
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
# set operating systems to test
os: [ubuntu-latest]
# set python versions to test
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]

name: test_Maps ${{ matrix.os }} ${{ matrix.python-version }}
steps:
Expand Down
Binary file modified docs/source/_static/example_images/example_inset_maps.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 40 additions & 39 deletions eomaps/_blit_manager.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -903,14 +903,14 @@ def _on_draw_cb(self, event):
if loglevel <= 5:
_log.log(5, "There was an error during draw!", exc_info=True)

def add_artist(self, art, layer=None):
def add_artist(self, *artists, layer=None):
"""
Add a dynamic-artist to be managed.
(Dynamic artists are re-drawn on every update!)

Parameters
----------
art : Artist
artists : Artist

The artist to be added. Will be set to 'animated' (just
to be safe). *art* must be in the figure associated with
Expand All @@ -922,38 +922,38 @@ def add_artist(self, art, layer=None):

The default is None in which case the layer of the base-Maps object is used.
"""

if art.figure != self.figure:
raise RuntimeError(
"EOmaps: The artist does not belong to the figure"
"of this Maps-object!"
)

if layer is None:
layer = self._m.layer

# make sure all layers are converted to string
layer = str(layer)

self._artists.setdefault(layer, list())
for art in artists:
if art.figure != self.figure:
raise RuntimeError(
"EOmaps: The artist does not belong to the figure"
"of this Maps-object!"
)

if art in self._artists[layer]:
return
else:
art.set_animated(True)
self._artists[layer].append(art)
self._artists.setdefault(layer, list())

if isinstance(art, plt.Axes):
self._managed_axes.add(art)
if art in self._artists[layer]:
continue
else:
art.set_animated(True)
self._artists[layer].append(art)

def add_bg_artist(self, art, layer=None, draw=True):
if isinstance(art, plt.Axes):
self._managed_axes.add(art)

def add_bg_artist(self, *artists, layer=None, draw=True):
"""
Add a background-artist to be managed.
(Background artists are only updated on zoom-events... they are NOT animated!)

Parameters
----------
art : Artist
artists : Artist
The artist to be added. Will be set to 'animated' (just
to be safe). *art* must be in the figure associated with
the canvas this class is managing.
Expand All @@ -974,31 +974,32 @@ def add_bg_artist(self, art, layer=None, draw=True):
# make sure all layer names are converted to string
layer = str(layer)

if art.figure != self.figure:
raise RuntimeError
for art in artists:
if art.figure != self.figure:
raise RuntimeError

# put all artist of inset-maps on dedicated layers
if (
getattr(art, "axes", None) is not None
and art.axes.get_label() == "inset_map"
and not layer.startswith("__inset_")
):
layer = "__inset_" + str(layer)
# put all artist of inset-maps on dedicated layers
if (
getattr(art, "axes", None) is not None
and art.axes.get_label() == "inset_map"
and not layer.startswith("__inset_")
):
layer = "__inset_" + str(layer)

if layer in self._bg_artists and art in self._bg_artists[layer]:
_log.info(
f"EOmaps: Background-artist '{art}' already added on layer '{layer}'"
)
return
if layer in self._bg_artists and art in self._bg_artists[layer]:
_log.info(
f"EOmaps: Background-artist '{art}' already added on layer '{layer}'"
)
continue

art.set_animated(True)
self._bg_artists.setdefault(layer, []).append(art)
art.set_animated(True)
self._bg_artists.setdefault(layer, []).append(art)

if isinstance(art, plt.Axes):
self._managed_axes.add(art)
if isinstance(art, plt.Axes):
self._managed_axes.add(art)

# tag all relevant layers for refetch
self._refetch_layer(layer)
# tag all relevant layers for refetch
self._refetch_layer(layer)

for f in self._on_add_bg_artist:
f()
Expand Down
4 changes: 2 additions & 2 deletions eomaps/_data_manager.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -916,9 +916,9 @@ def on_fetch_bg(self, layer=None, bbox=None, check_redraw=True):
self.m.ax.add_collection(coll, autolim=False)

if self.m._coll_dynamic:
self.m.BM.add_artist(coll, self.layer)
self.m.BM.add_artist(coll, layer=self.layer)
else:
self.m.BM.add_bg_artist(coll, self.layer)
self.m.BM.add_bg_artist(coll, layer=self.layer)

self.m._coll = coll

Expand Down
4 changes: 3 additions & 1 deletion eomaps/_maps_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from .layout_editor import LayoutEditor
from ._blit_manager import BlitManager
from .projections import Equi7Grid_projection # import also supercharges cartopy.ccrs
from ._zoom import LazyZoomMixin


def _handle_backends():
Expand Down Expand Up @@ -233,7 +234,7 @@ def handle_event(self, event):
FigureManagerWebAgg.refresh_all = refresh_all


class MapsBase(metaclass=_MapsMeta):
class MapsBase(LazyZoomMixin, metaclass=_MapsMeta):
def __init__(
self,
crs=None,
Expand Down Expand Up @@ -453,6 +454,7 @@ def _init_figure(self, **kwargs):
# variable of the parent Maps-object while keeping the figure open
# causes all weakrefs to be garbage-collected!
self.parent.f._EOmaps_parent = self.parent._real_self
self._connect_zoom_events()
else:
if not hasattr(self.parent.f, "_EOmaps_parent"):
self.parent.f._EOmaps_parent = self.parent._real_self
Expand Down
2 changes: 1 addition & 1 deletion eomaps/_webmap.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def add_legend(self, style=None, img=None):
if not self._m.BM._layer_visible(self._layer):
legax.set_visible(False)

self._m.BM.add_artist(legax, self._layer)
self._m.BM.add_artist(legax, layer=self._layer)

def cb_move(event):
if not self._legend_picked:
Expand Down
172 changes: 172 additions & 0 deletions eomaps/_zoom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
from matplotlib.transforms import Bbox
from types import SimpleNamespace
import numpy as np
from cartopy.mpl.geoaxes import GeoAxes


class LazyZoomMixin:
_zoom_scroll_scale_fine = 10
_zoom_scroll_scale_coarse = 50
_zoom_lazy_activator_key = " "

def _connect_zoom_events(self):
"""
Connect events for lazy-zoom and zoom via scroll-wheel

Returns
-------
cid_scroll, cid_keypress, cid_release: matplotlib callback IDs

"""
cid_scroll = self.f.canvas.mpl_connect(
"scroll_event", self._zoom_mousewheel_move
)
cid_keypress = self.f.canvas.mpl_connect(
"key_press_event", self._activate_lazy_zoom
)
cid_release = self.f.canvas.mpl_connect(
"key_release_event", self._deactivate_lazy_zoom
)

return (cid_scroll, cid_keypress, cid_release)

@staticmethod
def _add_lazy_zoom_axes_image(ax):
"""
Create a static image of the axes that is used for lazy-zooming.

Parameters
----------
ax : matplotlib.Axes
The axes to use.

Returns
-------
axi : matplotlib.Axes
The axes object containing the image used for zooming.

"""
x0, y0, x1, y1 = ax.bbox.bounds
(x0, y0), (x1, y1) = np.floor([x0, y0]), np.ceil([x1, y1])
bbox = Bbox.from_bounds(x0, y0, x1, y1)
ax._eomaps_img_buffer = ax.figure.canvas.copy_from_bbox(bbox)

axi = ax.figure.add_axes(ax.get_position())
axi.imshow(ax._eomaps_img_buffer, zorder=-100, alpha=0.75)
# axi.get_xaxis().set_visible(False)
# axi.get_yaxis().set_visible(False)
axi.tick_params(
which="both",
axis="both",
left=False,
right=False,
bottom=False,
top=False,
labelleft=False,
labelright=False,
labelbottom=False,
labeltop=False,
)
for _, s in axi.spines.items():
s.set_linewidth(2)
s.set_edgecolor("r")

axi.name = "eomaps_ax_image"
axi.eomaps_parent_ax = ax

axi.set_forward_navigation_events(True)
axi.set_zorder(-9999)

return axi

@staticmethod
def _check_auto_repeat_key(event):
"""
Check if a keypress event is triggered by auto-repeat.
(e.g. non-modifier keys tend to re-trigger automatically)

Parameters
----------
event : matplotlib.KeyPressEvent
The matpltolib event to check .

Returns
-------
bool: True if event is triggered by auto-repeat, else False

"""
# check if keypress event is triggered by auto-repeat
try:
return event.guiEvent.isAutoRepeat()
except Exception:
return False

def _zoom_mousewheel_move(self, event):
"""A callback to support zooming with the mouse-wheel."""
ax = event.inaxes

if not ax:
return

if event.key and "shift" in event.key:
scale = self._zoom_scroll_scale_coarse
else:
scale = self._zoom_scroll_scale_fine

axes = [ax]
if hasattr(ax, "_temp_zoom_ax"):
axes.append(ax._temp_zoom_ax)

for axi in axes:
axi._pan_start = SimpleNamespace(
lim=axi.viewLim.frozen(),
trans=axi.transData.frozen(),
trans_inverse=axi.transData.inverted().frozen(),
bbox=axi.bbox.frozen(),
x=event.x,
y=event.y,
)

if event.button == "up":
axi.drag_pan(3, event.key, event.x + scale, event.y + scale)
else:
axi.drag_pan(3, event.key, event.x - scale, event.y - scale)

self.redraw()

def _activate_lazy_zoom(self, event):
"""A callback to activate lazy-zooming."""

# ignore auto-repeat events to support ordinary keys as modifiers
if self._check_auto_repeat_key(event):
return

if event.key == self._zoom_lazy_activator_key:
for ax in self.f.axes:
if not isinstance(ax, GeoAxes):
continue

ax._temp_zoom_ax = self._add_lazy_zoom_axes_image(ax)

self.BM._disable_draw = True
self.BM._disable_update = True
self.f.canvas.draw_idle()

def _deactivate_lazy_zoom(self, event):
"""A callback to de-activate lazy-zooming."""

# ignore auto-repeat events to support ordinary keys as modifiers
if self._check_auto_repeat_key(event):
return

for ax in self.f.axes:
if hasattr(ax, "_temp_zoom_ax"):
try:
ax._temp_zoom_ax.remove()
del ax._temp_zoom_ax
except Exception:
continue

self.BM._disable_draw = False
self.BM._disable_update = False
self.redraw()
2 changes: 1 addition & 1 deletion eomaps/annotation_editor.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ def show_info_text(self):
fontfamily="monospace",
)

self.m.BM.add_artist(self._info_artist, "all")
self.m.BM.add_artist(self._info_artist, layer="all")

self._info_cids.add(
self.m.f.canvas.mpl_connect("button_press_event", self._on_press)
Expand Down
6 changes: 3 additions & 3 deletions eomaps/callbacks.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -654,11 +654,11 @@ def mark(
if permanent is False:
# make the annotation temporary
self._temporary_artists.append(marker)
self.m.BM.add_artist(marker, layer)
self.m.BM.add_artist(marker, layer=layer)
elif permanent is None:
self.m.BM.add_bg_artist(marker, layer)
self.m.BM.add_bg_artist(marker, layer=layer)
elif permanent is True:
self.m.BM.add_artist(marker, layer)
self.m.BM.add_artist(marker, layer=layer)

if not hasattr(self, "permanent_markers"):
self.permanent_markers = [marker]
Expand Down
Loading
Loading