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
17 changes: 15 additions & 2 deletions eomaps/_blit_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,8 @@ def __init__(self, m):
List of the artists to manage

"""
self._disable_draw = False
self._disable_update = False
self._disable_draw = set()
self._disable_update = set()

self._m = m
self._bg_layer = self._m.layer
Expand Down Expand Up @@ -252,6 +252,19 @@ def __init__(self, m):
# unmanaged artists
self._ignored_unmanaged_artists = WeakSet()

self._m.f.canvas._eomaps_draw_idle_orig = self._m.f.canvas.draw_idle

def getdrawidle(BM):
def draw_idle(self, *args, **kwargs):
if BM._disable_draw:
return

self._eomaps_draw_idle_orig()

return draw_idle

self._m.f.canvas.draw_idle = getdrawidle(self).__get__(self._m.f.canvas)

def _get_renderer(self):
# don't return the renderer if the figure is saved.
# in this case the normal draw-routines are used (see m.savefig) so there is
Expand Down
14 changes: 9 additions & 5 deletions eomaps/eomaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,15 +378,19 @@ def delay_draw(self):
>>> m.add_feature.preset.land()

"""
import uuid

uuid = str(uuid.uuid4())
try:
self.BM._disable_draw = True
self.BM._disable_update = True
self.BM._disable_draw.add(uuid)
self.BM._disable_update.add(uuid)

yield
finally:
self.BM._disable_draw = False
self.BM._disable_update = False
self.redraw()
self.BM._disable_draw.remove(uuid)
self.BM._disable_update.remove(uuid)
if not (self.BM._disable_draw or self.BM._disable_update):
self.redraw()

@property
def coll(self):
Expand Down
162 changes: 148 additions & 14 deletions eomaps/layout_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def add_info_text(self):
"0 - 9: Snap-grid spacing\n"
"SHIFT: Multi-select\n"
"P: Print to console\n"
"R: Re-draw backgrounds\n"
"ESCAPE (or ALT + L): Exit\n"
"\n"
"ARROW-KEYS: Move\n"
Expand Down Expand Up @@ -107,6 +108,7 @@ def _on_resize(self, *args, **kwargs):
if self.modifier_pressed:
self._add_snap_grid()
self._update_info_text()
self.blit_artists()

@property
def modifier_pressed(self):
Expand All @@ -119,11 +121,14 @@ def modifier_pressed(self, val):
self.m.cb.execute_callbacks(not val)

if self._modifier_pressed:
self.m.BM._disable_draw = True
self.m.BM._disable_update = True
for disable_set in ("_disable_draw", "_disable_update"):
getattr(self.m.BM, disable_set).add("layout_editor")
else:
self.m.BM._disable_draw = False
self.m.BM._disable_update = False
for disable_set in ("_disable_draw", "_disable_update"):
try:
getattr(self.m.BM, disable_set).remove("layout_editor")
except KeyError:
_log.debug(f"'layout_editor' key not found in {disable_set}")

@property
def ms(self):
Expand Down Expand Up @@ -328,7 +333,7 @@ def cb_pick(self, event):
self._m_picked = []
self._color_axes()
self._remove_snap_grid()
self.fetch_current_background()
# self.fetch_current_background()
self.blit_artists()
return

Expand Down Expand Up @@ -380,7 +385,7 @@ def cb_pick(self, event):

self._set_startpos(event)
self._color_axes()
self.fetch_current_background()
# self.fetch_current_background()
self.blit_artists()

def fetch_current_background(self):
Expand Down Expand Up @@ -457,13 +462,54 @@ def cb_move(self, event):
self._color_axes()
self.blit_artists()

def blit_artists(self):
artists = [*self._ax_picked]
def blit_artists(self, blit=True):
# render a white background
renderer = self.m.BM._get_renderer()
gc = renderer.new_gc()
w, h = self.f.bbox.width, self.f.bbox.height
renderer.draw_image(gc, 0, 0, np.full((int(h), int(w), 4), 255))
gc.restore()

if getattr(self, "_info_text", None) is not None:
artists.append(self._info_text)
# put snap-grid on top (if available)
grid = getattr(self, "_snap_grid_artist", None)
if grid is not None:
self.m.BM.blit_artists([grid], None, False)

# draw zoomed axes backgrounds
# get a "sorted" list of axes where the picked-axes is last
# to make sure it is blitted on top of all other axes
from scipy.ndimage import zoom

axes = reversed(dict.fromkeys((*self._ax_picked, *self.f.axes)))
for ax in axes:
if (curr_argb := getattr(ax, "_curr_argb", None)) is not None:
h0, w0 = curr_argb.shape[:2]
w0, h0 = ax._curr_size

bbox = ax.bbox
x0, y0, w, h = map(int, (bbox.x0, bbox.y0, bbox.width, bbox.height))

x0, y0 = np.floor([bbox.x0, bbox.y0]).astype(int)

argb = curr_argb.copy()
# make background image semi-transparent
argb[:, :, -1] = (argb[:, :, -1] * 0.5).astype(np.int8)

gc = renderer.new_gc()
gc.set_clip_rectangle(bbox)

argb = zoom(argb, (h / h0, w / w0, 1), order=0)

self.m.BM.blit_artists(artists, self._current_bg)
renderer.draw_image(gc, x0, y0, argb)
gc.restore()

self.m.BM.blit_artists(self.f.axes, None, blit=False)

if getattr(self, "_info_text", None) is not None and not self._info_text_hidden:
self.m.BM.blit_artists([self._info_text], None, blit=False)

if blit:
self.f.canvas.blit()

def cb_scroll(self, event):
if (self.f.canvas.toolbar is not None) and self.f.canvas.toolbar.mode != "":
Expand Down Expand Up @@ -511,6 +557,9 @@ def cb_scroll(self, event):
def cb_key_press(self, event):
# release shift key on every keypress
self._shift_pressed = False
if event.key == "r":
self.refetch_axes_images()
return

if (event.key == self.modifier) and (not self.modifier_pressed):
self._make_draggable()
Expand Down Expand Up @@ -623,7 +672,68 @@ def ax_on_layer(self, ax):

return False

def _make_draggable(self, filepath=None):
@staticmethod
def _checkboard_array(shape, blocksize=(10, 10), a=100, b=50):
rx, ry = np.ceil(np.array(shape) / blocksize / 2).astype(int)
return np.kron([[a, b] * ry, [b, a] * ry] * rx, np.ones(blocksize))[
: shape[0], : shape[1]
]

def _make_draggable(self, filepath=None, blit=True):
fig_buffer = self.m.BM._get_active_bg()
x = fig_buffer.get_extents()
ncols, nrows = x[2] - x[0], x[3] - x[1]

argb = (
np.frombuffer(fig_buffer, dtype=np.uint8).reshape((nrows, ncols, 4)).copy()
)[::-1, :, :]

for ax in self.f.axes:
bbox = ax.bbox
(x0, y0), (x1, y1) = (
np.floor([bbox.x0, bbox.y0]).astype(int),
np.ceil([bbox.x1, bbox.y1]).astype(int),
)

# get dummy background image in case axes is partly outside of figure
full_shp = np.ceil((ax.bbox.height, ax.bbox.width)).astype(int)

argb_full = np.stack(
[
*[self._checkboard_array(full_shp, (10, 10), 255, 100)] * 3,
np.full(full_shp, 255),
],
axis=2,
)

# clip axes bounds with respect to figure extent
x0c, x1c = np.clip((x0, x1), 0, int(np.ceil(self.m.f.bbox.width)))
y0c, y1c = np.clip((y0, y1), 0, int(np.ceil(self.m.f.bbox.height)))

# get axes image pixel values
curr_argb = argb[y0c:y1c, x0c:x1c, :]

# get shifts of the axes rel. to clip image
sx, sy = x0c - x0, y0c - y0
# get slices
slx = slice(sx, sx + curr_argb.shape[1])
sly = slice(sy, sy + curr_argb.shape[0])

# TODO implement proper rounding of bbox bounds
# to avoid issues with single-pixel shifts we evaluate the shape first and
# make sure the array is clipped to the size
overlap_shp = argb_full[sly, slx, ...].shape
curr_argb = curr_argb[: overlap_shp[0], : overlap_shp[1], ...]

# fill values with image-values if available
argb_full[sly, slx, ...] = curr_argb

# remember background
ax._curr_argb = argb_full
ax._curr_size = ax.bbox.width, ax.bbox.height

self.f.set_visible(False)

# Uncheck active pan/zoom actions of the matplotlib toolbar.
# use a try-except block to avoid issues with ipympl in jupyter notebooks
# (see https://github.com/matplotlib/ipympl/issues/530#issue-1780919042)
Expand Down Expand Up @@ -721,7 +831,7 @@ def _make_draggable(self, filepath=None):
self._add_revert_props(child, "facecolor")

# make sure patches are visible (and re-drawn on draw)
child.set_visible(True)
child.set_visible(False)
child.set_facecolor("w")
child.set_alpha(0.75) # for overlapping axes

Expand All @@ -734,12 +844,15 @@ def _make_draggable(self, filepath=None):
if getattr(self, "_info_text", None) is None:
self._info_text = self.add_info_text()

self.fetch_current_background()

self._color_axes()
self._attach_callbacks()

self.m._emit_signal("layoutEditorActivated")

self.m.redraw()
if blit:
self.blit_artists()

def _add_revert_props(self, child, *args):
for prop in args:
Expand All @@ -752,6 +865,7 @@ def _add_revert_props(self, child, *args):
)

def _undo_draggable(self):
self.m.f.set_visible(True)
if getattr(self, "_info_text", None) not in (None, False):
self._info_text.remove()
# set to None to avoid crating the info-text again
Expand Down Expand Up @@ -821,6 +935,25 @@ def _undo_draggable(self):
self.m.f.canvas.toolbar.push_current()
except Exception:
pass
self.m.f.canvas.draw()

def refetch_axes_images(self):
# remember visibility of info-textbox
if self._info_text and self._info_text.get_visible() is False:
info_txt_hidden = True
else:
info_txt_hidden = False

self._undo_draggable()
newbg = self.m.f.canvas.copy_from_bbox(self.m.f.bbox)
self._make_draggable(blit=False)

# hide info-text in case it was hidden before
if self._info_text and info_txt_hidden:
self._info_text.set_visible(False)
self._info_text_hidden = True

self.m.f.canvas.restore_region(newbg)

def _reset_callbacks(self):
# disconnect all callbacks of the layout-editor
Expand Down Expand Up @@ -874,6 +1007,7 @@ def _add_snap_grid(self, snap=None):
markerfacecolor="steelblue",
markeredgecolor="none",
ms=(snapx + snapy) / 6,
animated=True,
)
self._snap_grid_artist = self.m.f.add_artist(l)

Expand Down
2 changes: 1 addition & 1 deletion examples/callbacks/location_indicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# create a new map
m = Maps(Maps.CRS.Robinson(), figsize=(8, 4))
m.add_feature.preset.ocean()
gl = m.add_gridlines(d=5, lw=0.25, ls=":")
m.add_gridlines(d=5, lw=0.25, ls=":")


def cb_location_indicator_grid(pos, **kwargs):
Expand Down
Loading