Skip to content

Commit

Permalink
Merge pull request #191 from raphaelquast/dev
Browse files Browse the repository at this point in the history
Merge for v7.3.1
  • Loading branch information
raphaelquast authored Nov 19, 2023
2 parents 917e44a + 1feedc8 commit f74f6e3
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 72 deletions.
Binary file modified docs/_static/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.
2 changes: 1 addition & 1 deletion eomaps/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "7.3"
__version__ = "7.3.1"
9 changes: 5 additions & 4 deletions eomaps/eomaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -1688,9 +1688,7 @@ def add_title(self, title, x=0.5, y=1.01, **kwargs):
kwargs.setdefault("horizontalalignment", "center")
kwargs.setdefault("verticalalignment", "bottom")

self.text(
0.5, 1.01, title, transform=self.ax.transAxes, layer=self.layer, **kwargs
)
self.text(x, y, title, transform=self.ax.transAxes, layer=self.layer, **kwargs)

@lru_cache()
def get_crs(self, crs="plot"):
Expand Down Expand Up @@ -5647,7 +5645,11 @@ def set_frame(self, rounded=0, **kwargs):
>>> path_effects=[pe.withStroke(linewidth=7, foreground="m")])
"""
self.redraw("__SPINES__")

for key in ("fc", "facecolor"):
self.redraw("__BG__")

if key in kwargs:
self.ax.patch.set_facecolor(kwargs.pop(key))

Expand Down Expand Up @@ -5710,4 +5712,3 @@ def cb(*args, **kwargs):

self.BM._before_fetch_bg_actions.append(cb)
self.ax._EOmaps_rounded_spine_attached = True
self.BM.update()
81 changes: 47 additions & 34 deletions eomaps/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,39 @@
_log = logging.getLogger(__name__)


def _ccw(A, B, C):
# determine if 3 points are listed in a counter-clockwise order
return (C[..., 1] - A[..., 1]) * (B[..., 0] - A[..., 0]) > (
B[..., 1] - A[..., 1]
) * (C[..., 0] - A[..., 0])


def _intersect(A, B, C, D):
# determine if 2 line-segments intersect with each other
# see https://stackoverflow.com/a/9997374/9703451
# see https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/

A, B, C, D = map(np.atleast_2d, (A, B, C, D))
return np.logical_and(
_ccw(A, C, D) != _ccw(B, C, D),
_ccw(A, B, C) != _ccw(A, B, D),
)


def _get_intersect(a1, a2, b1, b2):
# get the intersection-point between 2 lines defined by points
# taken from https://stackoverflow.com/a/42727584/9703451

s = np.vstack([a1, a2, b1, b2]) # s for stacked
h = np.hstack((s, np.ones((4, 1)))) # h for homogeneous
l1 = np.cross(h[0], h[1]) # get first line
l2 = np.cross(h[2], h[3]) # get second line
x, y, z = np.cross(l1, l2) # point of intersection
if z == 0: # lines are parallel
return (float("inf"), float("inf"))
return (x / z, y / z)


class GridLines:
"""Class to draw grid-lines."""

Expand Down Expand Up @@ -361,8 +394,15 @@ def remove(self):
"""Remove the grid from the map."""
self._remove()

if self in self.m._grid._gridlines:
self.m._grid._gridlines.remove(self)
factory = self.m.parent._grid
if self in factory._gridlines:
factory._gridlines.remove(self)

for gl in self._grid_labels:
gl.remove()

if self._dynamic is False:
self.m.redraw(self.layer)

def add_labels(
self,
Expand Down Expand Up @@ -467,6 +507,9 @@ def add_labels(
# remember attached labels
self._grid_labels.append(gl)

if self._dynamic is False:
self.m.redraw(self.layer)

return gl


Expand Down Expand Up @@ -558,36 +601,6 @@ def _set_offset(self, offset):
def _set_rotation(self, rotation):
self._rotation = np.deg2rad(rotation)

def _ccw(self, A, B, C):
# determine if 3 points are listed in a counter-clockwise order
return (C[..., 1] - A[..., 1]) * (B[..., 0] - A[..., 0]) > (
B[..., 1] - A[..., 1]
) * (C[..., 0] - A[..., 0])

def _intersect(self, A, B, C, D):
# determine if 2 line-segments intersect with each other
# see https://stackoverflow.com/a/9997374/9703451
# see https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/

A, B, C, D = map(np.atleast_2d, (A, B, C, D))
return np.logical_and(
self._ccw(A, C, D) != self._ccw(B, C, D),
self._ccw(A, B, C) != self._ccw(A, B, D),
)

def _get_intersect(self, a1, a2, b1, b2):
# get the intersection-point between 2 lines defined by points
# taken from https://stackoverflow.com/a/42727584/9703451

s = np.vstack([a1, a2, b1, b2]) # s for stacked
h = np.hstack((s, np.ones((4, 1)))) # h for homogeneous
l1 = np.cross(h[0], h[1]) # get first line
l2 = np.cross(h[2], h[3]) # get second line
x, y, z = np.cross(l1, l2) # point of intersection
if z == 0: # lines are parallel
return (float("inf"), float("inf"))
return (x / z, y / z)

def _remove(self):
while len(self._texts) > 0:
try:
Expand Down Expand Up @@ -734,10 +747,10 @@ def _get_spine_intersections(self, lines, axis=None):
b0 = np.stack((b0x, b0y), axis=2)
b1 = np.stack((b1x, b1y), axis=2)

q = self._intersect(l0, l1, b0, b1)
q = _intersect(l0, l1, b0, b1)

for la, lb, ba, bb in zip(l0[q], l1[q], b0[q], b1[q]):
x, y = self._get_intersect(la, lb, ba, bb)
x, y = _get_intersect(la, lb, ba, bb)

xt, yt = tr_ax.transform((x, y))

Expand Down
3 changes: 3 additions & 0 deletions eomaps/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1870,6 +1870,9 @@ def cb(*args, **kwargs):
if layer is None:
self._on_layer_change[persistent].append(cb)
else:
# treat inset-map layers like normal layers
if layer.startswith("__inset_"):
layer = layer[8:]
self._on_layer_activation[persistent].setdefault(layer, list()).append(cb)

def _refetch_layer(self, layer):
Expand Down
41 changes: 30 additions & 11 deletions eomaps/inset_maps.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from . import Maps
from .helpers import _deprecated
from .grid import _intersect, _get_intersect


class InsetMaps(Maps):
Expand Down Expand Up @@ -72,7 +73,7 @@ def __init__(
# use same edgecolor for boundary and indicator by default
self._extent_kwargs["ec"] = boundary["ec"]
self._line_kwargs["c"] = boundary["ec"]
elif isinstance(boundary, str):
elif isinstance(boundary, (str, tuple)):
boundary_kwargs.update({"ec": boundary})
# use same edgecolor for boundary and indicator by default
self._extent_kwargs["ec"] = boundary
Expand Down Expand Up @@ -305,7 +306,7 @@ def add_indicator_line(self, m=None, **kwargs):

# This is because all artists on inset-map axes are always on top of other
# (normal map) artists... (and so the line would be behind the background)
from matplotlib.transforms import Path, TransformedPath
from matplotlib.transforms import TransformedPath

clip_path = TransformedPath(
m.ax.patch.get_path(), m.ax.projection._as_mpl_transform(m.ax)
Expand All @@ -324,19 +325,37 @@ def add_indicator_line(self, m=None, **kwargs):
self.BM._before_fetch_bg_actions.append(self._update_indicator_lines)

def _update_indicator_lines(self, *args, **kwargs):
verts = self._get_spine_verts().mean(axis=0)
spine_verts = self._get_spine_verts()

verts_t = np.column_stack(self._transf_lonlat_to_plot.transform(*verts.T))
verts_t = (self.ax.transData + self.f.transFigure.inverted()).transform(verts_t)
x1, y1 = verts_t[0]
# find center of the inset map in the figure (in figure coordinates)
verts = np.column_stack(self._transf_lonlat_to_plot.transform(*spine_verts.T))
verts = (self.ax.transData + self.f.transFigure.inverted()).transform(verts)

for l, m in self._indicator_lines:
verts_t = np.column_stack(m._transf_lonlat_to_plot.transform(*verts.T))
# find center of inset-map indicator on the map (in figure coordinates)
verts_t = np.column_stack(
m._transf_lonlat_to_plot.transform(*spine_verts.T)
)
verts_t = (m.ax.transData + m.f.transFigure.inverted()).transform(verts_t)
x0, y0 = verts_t[0]

l.set_xdata([x0, x1])
l.set_ydata([y0, y1])
p_map = verts_t.mean(axis=0)

p_inset = verts.mean(axis=0)
# find intersection points of lines connecting the centers
# 1) with the inset-map boundary
q = _intersect(p_map, p_inset, verts[:-1], verts[1:])
if q.any():
x0, y0 = _get_intersect(p_map, p_inset, verts[:-1][q], verts[1:][q])
else:
x0, y0 = p_inset

# 2) with the inset-map indicator on the map
q = _intersect(p_map, p_inset, verts_t[:-1], verts_t[1:])
if q.any():
x1, y1 = _get_intersect(p_map, p_inset, verts_t[:-1][q], verts_t[1:][q])
# update indicator line vertices
l.set_xdata([x0, x1])
l.set_ydata([y0, y1])
continue

# a convenience-method to set the position based on the center of the axis
def set_inset_position(self, x=None, y=None, size=None):
Expand Down
43 changes: 21 additions & 22 deletions tests/example_inset_maps.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

# ---------- create a new inset-map
# showing a 15 degree rectangle around the xy-point
m2 = m.new_inset_map(
mi1 = m.new_inset_map(
xy=(5, 45),
xy_crs=4326,
shape="rectangles",
Expand All @@ -18,17 +18,14 @@
indicate_extent=dict(fc=(1, 0, 0, 0.25)),
)

# populate the inset with some more detailed features
m2.add_feature.preset.coastline()
m2.add_feature.preset.ocean()
m2.add_feature.preset.land()
m2.add_feature.preset.countries()
m2.add_feature.preset.urban_areas()
mi1.add_indicator_line(m, marker="o")

# populate the inset with some more detailed features
mi1.add_feature.preset("coastline", "ocean", "land", "countries", "urban_areas")

# ---------- create another inset-map
# showing a 400km circle around the xy-point
m3 = m.new_inset_map(
mi2 = m.new_inset_map(
xy=(5, 45),
xy_crs=4326,
shape="geod_circles",
Expand All @@ -39,10 +36,11 @@
boundary=dict(ec="g", lw=2),
indicate_extent=dict(fc=(0, 1, 0, 0.25)),
)
mi2.add_indicator_line(m, marker="o")

# populate the inset with some features
m3.add_feature.preset("ocean", "land")
m3.add_feature.preset.urban_areas(zorder=1)
mi2.add_feature.preset("ocean", "land")
mi2.add_feature.preset.urban_areas(zorder=1)

# print some data on all of the maps

Expand All @@ -54,25 +52,26 @@
m.plot_map(alpha=0.5, ec="none", set_extent=False)

# use the same data and classification for the inset-maps
for m_i in [m2, m3]:
for m_i in [mi1, mi2]:
m_i.inherit_data(m)
m_i.inherit_classification(m)

m2.set_shape.ellipses(np.mean(m.shape.radius) / 2)
m2.plot_map(alpha=0.75, ec="k", lw=0.5, set_extent=False)
mi1.set_shape.ellipses(np.mean(m.shape.radius) / 2)
mi1.plot_map(alpha=0.75, ec="k", lw=0.5, set_extent=False)

m3.set_shape.ellipses(np.mean(m.shape.radius) / 2)
m3.plot_map(alpha=1, ec="k", lw=0.5, set_extent=False)
mi2.set_shape.ellipses(np.mean(m.shape.radius) / 2)
mi2.plot_map(alpha=1, ec="k", lw=0.5, set_extent=False)


# add an annotation for the second datapoint to the inset-map
m3.add_annotation(ID=1, xytext=(-120, 80))
mi2.add_annotation(ID=1, xytext=(-120, 80))

# indicate the extent of the second inset on the first inset
m3.add_extent_indicator(m2, ec="g", lw=2, fc="g", alpha=0.5, zorder=0)
mi2.add_extent_indicator(mi1, ec="g", lw=2, fc="g", alpha=0.5, zorder=0)
mi2.add_indicator_line(mi1, marker="o")

# add some additional text to the inset-maps
for m_i, txt, color in zip([m2, m3], ["epsg: 4326", "epsg: 3035"], ["r", "g"]):
for m_i, txt, color in zip([mi1, mi2], ["epsg: 4326", "epsg: 3035"], ["r", "g"]):
txt = m_i.ax.text(
0.5,
0,
Expand All @@ -84,14 +83,14 @@
# add the text-objects as artists to the blit-manager
m_i.BM.add_artist(txt)

m3.add_colorbar(hist_bins=20, margin=dict(bottom=-0.2), label="some parameter")
mi2.add_colorbar(hist_bins=20, margin=dict(bottom=-0.2), label="some parameter")
# move the inset map (and the colorbar) to a different location
m3.set_inset_position(x=0.3)
mi2.set_inset_position(x=0.3)

# share pick events
for mi in [m, m2, m3]:
for mi in [m, mi1, mi2]:
mi.cb.pick.attach.annotate(text=lambda ID, val, **kwargs: f"ID={ID}\nval={val:.2f}")
m.cb.pick.share_events(m2, m3)
m.cb.pick.share_events(mi1, mi2)

m.apply_layout(
{
Expand Down

0 comments on commit f74f6e3

Please sign in to comment.