Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for rotating mpl selectors of Ellipse,RectanglePixelRegion #390

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
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
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ New Features

- Added the DS9 'boxcircle' point symbol. [#387]

- Enable rotation of the ``as_mpl_selector`` widgets for rectangular
and ellipse regions with matplotlib versions supporting this. [#390]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will need to be be moved to version 0.8.

- Added the ability to add and subtract ``PixCoord`` objects. [#396]

- Added an ``origin`` keyword to ``PolygonPixelRegion`` to allow
Expand Down
43 changes: 29 additions & 14 deletions regions/shapes/ellipse.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@
xy = self.center.x - origin[0], self.center.y - origin[1]
width = self.width
height = self.height
# matplotlib expects rotation in degrees (anti-clockwise)
# matplotlib expects rotation in degrees (counter-clockwise)
angle = self.angle.to('deg').value

mpl_kwargs = self.visual.define_mpl_kwargs(self._mpl_artist)
Expand All @@ -210,13 +210,23 @@
**mpl_kwargs)

def _update_from_mpl_selector(self, *args, **kwargs):
xmin, xmax, ymin, ymax = self._mpl_selector.extents
self.center = PixCoord(x=0.5 * (xmin + xmax),
y=0.5 * (ymin + ymax))
self.width = (xmax - xmin)
self.height = (ymax - ymin)
self.angle = 0. * u.deg
if self._mpl_selector_callback is not None:
# matplotlib#26833 removed _rect_properties / _rect_bbox,
# ``selector.extents`` are now following rotation, i.e. giving min/max x and y
# for the _rotated_ rectangle. For proper width and height use bbox edge distances
# in both dimensions (take 0|2 as lower|upper x, 1|3 as lower|upper y edge).

xec, yec = self._mpl_selector.edge_centers
self.width = np.sqrt((xec[2] - xec[0])**2 + (yec[2] - yec[0])**2)
self.height = np.sqrt((xec[3] - xec[1])**2 + (yec[3] - yec[1])**2)
self.center = PixCoord(*self._mpl_selector.center)
# matplotlib defines rotation counter-clockwise (available from 3.6.0 on)
if hasattr(self._mpl_selector, 'rotation'):
rotation = -self._mpl_selector.rotation
else:
rotation = 0

Check warning on line 226 in regions/shapes/ellipse.py

View check run for this annotation

Codecov / codecov/patch

regions/shapes/ellipse.py#L226

Added line #L226 was not covered by tests
self.angle = rotation * u.deg

if getattr(self, '_mpl_selector_callback', None) is not None:
self._mpl_selector_callback(self)

def as_mpl_selector(self, ax, active=True, sync=True, callback=None,
Expand Down Expand Up @@ -261,13 +271,15 @@
you can enable/disable the selector at any point by calling
``selector.set_active(True)`` or ``selector.set_active(False)``.
"""
from matplotlib import __version__ as MPL_VER_STR # noqa: N812
from matplotlib.widgets import EllipseSelector

if hasattr(self, '_mpl_selector'):
raise AttributeError('Cannot attach more than one selector to a region.')

if self.angle.value != 0:
raise NotImplementedError('Cannot create matplotlib selector for rotated ellipse.')
if self.angle.value != 0 and not hasattr(EllipseSelector, 'rotation'):
raise NotImplementedError('Creating selectors for rotated shapes is not '
f'yet supported with matplotlib {MPL_VER_STR}.')

if sync:
sync_callback = self._update_from_mpl_selector
Expand All @@ -286,10 +298,13 @@
ax, sync_callback, interactive=True,
drag_from_anywhere=drag_from_anywhere, **kwargs)

self._mpl_selector.extents = (self.center.x - self.width / 2,
self.center.x + self.width / 2,
self.center.y - self.height / 2,
self.center.y + self.height / 2)
xy0 = [self.center.x - self.width / 2, self.center.y - self.height / 2]
self._mpl_selector.extents = (xy0[0], self.center.x + self.width / 2,
xy0[1], self.center.y + self.height / 2)

if self.angle.value != 0:
self._mpl_selector.rotation = -self.angle.to_value('deg')

self._mpl_selector.set_active(active)
self._mpl_selector_callback = callback

Expand Down
44 changes: 29 additions & 15 deletions regions/shapes/rectangle.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@
xy = xy[0] - origin[0], xy[1] - origin[1]
width = self.width
height = self.height
# matplotlib expects rotation in degrees (anti-clockwise)
# matplotlib expects rotation in degrees (counter-clockwise)
angle = self.angle.to('deg').value

mpl_kwargs = self.visual.define_mpl_kwargs(self._mpl_artist)
Expand All @@ -206,13 +206,23 @@
angle=angle, **mpl_kwargs)

def _update_from_mpl_selector(self, *args, **kwargs):
xmin, xmax, ymin, ymax = self._mpl_selector.extents
self.center = PixCoord(x=0.5 * (xmin + xmax),
y=0.5 * (ymin + ymax))
self.width = (xmax - xmin)
self.height = (ymax - ymin)
self.angle = 0. * u.deg
if self._mpl_selector_callback is not None:
# matplotlib#26833 removed _rect_properties / _rect_bbox,
# ``selector.extents`` are now following rotation, i.e. giving min/max x and y
# for the _rotated_ rectangle. For proper width and height use edge distances
# in both dimensions (take 0|2 as lower|upper x, 1|3 as lower|upper y edge).

xec, yec = self._mpl_selector.edge_centers
self.width = np.sqrt((xec[2] - xec[0])**2 + (yec[2] - yec[0])**2)
self.height = np.sqrt((xec[3] - xec[1])**2 + (yec[3] - yec[1])**2)
self.center = PixCoord(*self._mpl_selector.center)
# matplotlib defines rotation counter-clockwise (available from 3.6.0 on)
if hasattr(self._mpl_selector, 'rotation'):
rotation = -self._mpl_selector.rotation
else:
rotation = 0

Check warning on line 222 in regions/shapes/rectangle.py

View check run for this annotation

Codecov / codecov/patch

regions/shapes/rectangle.py#L222

Added line #L222 was not covered by tests
self.angle = rotation * u.deg

if getattr(self, '_mpl_selector_callback', None) is not None:
self._mpl_selector_callback(self)

def as_mpl_selector(self, ax, active=True, sync=True, callback=None,
Expand Down Expand Up @@ -257,14 +267,15 @@
you can enable/disable the selector at any point by calling
``selector.set_active(True)`` or ``selector.set_active(False)``.
"""
from matplotlib import __version__ as MPL_VER_STR # noqa: N812
from matplotlib.widgets import RectangleSelector

if hasattr(self, '_mpl_selector'):
raise AttributeError('Cannot attach more than one selector to a region.')

if self.angle.value != 0:
raise NotImplementedError('Cannot create matplotlib selector for '
'rotated rectangle.')
if self.angle.value != 0 and not hasattr(RectangleSelector, 'rotation'):
raise NotImplementedError('Creating selectors for rotated shapes is not '
f'yet supported with matplotlib {MPL_VER_STR}.')

if sync:
sync_callback = self._update_from_mpl_selector
Expand All @@ -283,10 +294,13 @@
ax, sync_callback, interactive=True,
drag_from_anywhere=drag_from_anywhere, **kwargs)

self._mpl_selector.extents = (self.center.x - self.width / 2,
self.center.x + self.width / 2,
self.center.y - self.height / 2,
self.center.y + self.height / 2)
dxy = [self.width / 2, self.height / 2]
self._mpl_selector.extents = (self.center.x - dxy[0], self.center.x + dxy[0],
self.center.y - dxy[1], self.center.y + dxy[1])

if self.angle.value != 0:
self._mpl_selector.rotation = -self.angle.to_value('deg')

self._mpl_selector.set_active(active)
self._mpl_selector_callback = callback

Expand Down
Loading