Skip to content

Commit c4246fb

Browse files
authored
Merge pull request #173 from mwcraig/update-marker-access
Update the interface and ginga implementation to make some changes of the marker API
2 parents be480c2 + 8806489 commit c4246fb

File tree

2 files changed

+85
-29
lines changed

2 files changed

+85
-29
lines changed

astrowidgets/interface_definition.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@
1212
# Allowed locations for cursor display
1313
ALLOWED_CURSOR_LOCATIONS = ('top', 'bottom', None)
1414

15+
DEFAULT_MARKER_NAME = 'default-marker-name'
16+
DEFAULT_INTERACTIVE_MARKER_NAME = 'interactive-markers'
17+
1518
# List of marker names that are for internal use only
1619
RESERVED_MARKER_SET_NAMES = ('all',)
1720

1821
__all__ = [
1922
'ImageViewerInterface',
2023
'ALLOWED_CURSOR_LOCATIONS',
21-
'RESERVED_MARKER_SET_NAMES'
24+
'RESERVED_MARKER_SET_NAMES',
25+
'DEFAULT_MARKER_NAME',
26+
'DEFAULT_INTERACTIVE_MARKER_NAME'
2227
]
2328

2429

@@ -48,6 +53,12 @@ class ImageViewerInterface(Protocol):
4853
# List of marker names that are for internal use only
4954
RESERVED_MARKER_SET_NAMES: tuple = RESERVED_MARKER_SET_NAMES
5055

56+
# Default marker name for marking via API
57+
DEFAULT_MARKER_NAME: str = DEFAULT_MARKER_NAME
58+
59+
# Default marker name for interactive marking
60+
DEFAULT_INTERACTIVE_MARKER_NAME: str = DEFAULT_INTERACTIVE_MARKER_NAME
61+
5162
# The methods, grouped loosely by purpose
5263

5364
# Methods for loading data
@@ -179,15 +190,15 @@ def reset_markers(self) -> None:
179190
# raise NotImplementedError
180191

181192
@abstractmethod
182-
def remove_markers(self, marker_name: str | None = None) -> None:
193+
def remove_markers(self, marker_name: str | list[str] | None = None) -> None:
183194
"""
184195
Remove markers from the image.
185196
186197
Parameters
187198
----------
188199
marker_name : str, optional
189-
The name of the marker set to remove. If not given, all
190-
markers will be removed.
200+
The name of the marker set to remove. If the value is ``"all"``,
201+
then all markers will be removed.
191202
"""
192203
raise NotImplementedError
193204

@@ -198,7 +209,7 @@ def remove_markers(self, marker_name: str | None = None) -> None:
198209
@abstractmethod
199210
def get_markers(self, x_colname: str = 'x', y_colname: str = 'y',
200211
skycoord_colname: str = 'coord',
201-
marker_name: str | None = None) -> Table:
212+
marker_name: str | list[str] | None = None) -> Table:
202213
"""
203214
Get the marker positions.
204215
@@ -213,14 +224,15 @@ def get_markers(self, x_colname: str = 'x', y_colname: str = 'y',
213224
skycoord_colname : str, optional
214225
The name of the column containing the sky coordinates. Default
215226
is ``'coord'``.
216-
marker_name : str, optional
217-
The name of the marker set to use. If not given, all
218-
markers will be returned.
227+
marker_name : str or list of str, optional
228+
The name of the marker set to use. If that value is ``"all"``,
229+
then all markers will be returned.
219230
220231
Returns
221232
-------
222233
table : `astropy.table.Table`
223-
The table containing the marker positions.
234+
The table containing the marker positions. If no markers match the
235+
``marker_name`` parameter, an empty table is returned.
224236
"""
225237
raise NotImplementedError
226238

astrowidgets/tests/widget_api_test.py

Lines changed: 64 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# TODO: How to enable switching out backend and still run the same tests?
2+
import warnings
23

34
import pytest
45

@@ -46,6 +47,11 @@ class variable does the trick.
4647
"""
4748
self.image = self.image_widget_class(image_width=250, image_height=100)
4849

50+
def _check_marker_table_return_properties(self, table):
51+
assert isinstance(table, Table)
52+
assert len(table) == 0
53+
assert sorted(table.colnames) == sorted(['x', 'y', 'coord', 'marker name'])
54+
4955
def test_width_height(self):
5056
assert self.image.image_width == 250
5157
assert self.image.image_height == 100
@@ -103,7 +109,7 @@ def test_zoom(self):
103109

104110
def test_marking_operations(self):
105111
marks = self.image.get_markers(marker_name="all")
106-
assert marks is None
112+
self._check_marker_table_return_properties(marks)
107113
assert not self.image.is_marking
108114

109115
# Ensure you cannot set it like this.
@@ -138,12 +144,13 @@ def test_marking_operations(self):
138144
assert not self.image.click_drag
139145
assert not self.image.scroll_pan
140146

141-
# Regression test for GitHub Issue 97:
142-
# Marker name with no markers should give warning.
143-
with pytest.warns(UserWarning, match='is empty') as warning_lines:
147+
# Make sure no warning is issued when trying to retrieve markers
148+
# with a name that does not exist.
149+
with warnings.catch_warnings():
150+
warnings.simplefilter("error")
144151
t = self.image.get_markers(marker_name='markymark')
145-
assert t is None
146-
assert len(warning_lines) == 1
152+
153+
self._check_marker_table_return_properties(t)
147154

148155
self.image.click_drag = True
149156
self.image.start_marking()
@@ -160,7 +167,7 @@ def test_marking_operations(self):
160167
self.image.stop_marking(clear_markers=True)
161168

162169
assert self.image.is_marking is False
163-
assert self.image.get_markers(marker_name="all") is None
170+
self._check_marker_table_return_properties(self.image.get_markers(marker_name="all"))
164171

165172
# Hate this, should add to public API
166173
marknames = self.image._marktags
@@ -170,8 +177,7 @@ def test_marking_operations(self):
170177
assert self.image.click_drag
171178

172179
def test_add_markers(self):
173-
rng = np.random.default_rng(1234)
174-
data = rng.integers(0, 100, (5, 2))
180+
data = np.arange(10).reshape(5, 2)
175181
orig_tab = Table(data=data, names=['x', 'y'], dtype=('float', 'float'))
176182
tab = Table(data=data, names=['x', 'y'], dtype=('float', 'float'))
177183
self.image.add_markers(tab, x_colname='x', y_colname='y',
@@ -234,35 +240,73 @@ def test_add_markers(self):
234240
self.image.reset_markers()
235241
marknames = self.image._marktags
236242
assert len(marknames) == 0
237-
assert self.image.get_markers(marker_name="all") is None
238-
with pytest.warns(UserWarning, match='is empty'):
239-
assert self.image.get_markers(marker_name=self.image._default_mark_tag_name) is None
243+
self._check_marker_table_return_properties(self.image.get_markers(marker_name="all"))
244+
# Check that no markers remain after clearing
245+
tab = self.image.get_markers(marker_name=self.image._default_mark_tag_name)
246+
self._check_marker_table_return_properties(tab)
247+
248+
# Check that retrieving a marker set that doesn't exist returns
249+
# an empty table with the right columns
250+
tab = self.image.get_markers(marker_name='test1')
251+
self._check_marker_table_return_properties(tab)
252+
253+
def test_get_markers_accepts_list_of_names(self):
254+
# Check that the get_markers method accepts a list of marker names
255+
# and returns a table with all the markers from all the named sets.
256+
data = np.arange(10).reshape((5, 2))
257+
tab = Table(data=data, names=['x', 'y'])
258+
self.image.add_markers(tab, marker_name='test1')
259+
self.image.add_markers(tab, marker_name='test2')
240260

241-
with pytest.raises(ValueError, match="No markers named 'test1'"):
242-
self.image.get_markers(marker_name='test1')
243-
with pytest.raises(ValueError, match="No markers named 'test2'"):
244-
self.image.get_markers(marker_name='test2')
261+
# No guarantee markers will come back in the same order, so sort them.
262+
t1 = self.image.get_markers(marker_name=['test1', 'test2'])
263+
# Sort before comparing
264+
t1.sort('x')
265+
expected = vstack([tab, tab], join_type='exact')
266+
expected.sort('x')
267+
np.testing.assert_array_equal(t1['x'], expected['x'])
268+
np.testing.assert_array_equal(t1['y'], expected['y'])
245269

246270
def test_remove_markers(self):
247271
with pytest.raises(ValueError, match='arf'):
248272
self.image.remove_markers(marker_name='arf')
249273

274+
def test_remove_markers_name_all(self):
275+
data = np.arange(10).reshape(5, 2)
276+
tab = Table(data=data, names=['x', 'y'])
277+
self.image.add_markers(tab, marker_name='test1')
278+
self.image.add_markers(tab, marker_name='test2')
279+
280+
self.image.remove_markers(marker_name='all')
281+
self._check_marker_table_return_properties(self.image.get_markers(marker_name='all'))
282+
283+
def test_remove_marker_accepts_list(self):
284+
data = np.arange(10).reshape(5, 2)
285+
tab = Table(data=data, names=['x', 'y'])
286+
self.image.add_markers(tab, marker_name='test1')
287+
self.image.add_markers(tab, marker_name='test2')
288+
289+
self.image.remove_markers(marker_name=['test1', 'test2'])
290+
marks = self.image.get_markers(marker_name='all')
291+
self._check_marker_table_return_properties(marks)
292+
250293
def test_adding_markers_as_world(self, data, wcs):
251294
ndd = NDData(data=data, wcs=wcs)
252295
self.image.load_nddata(ndd)
253296

254297
# Add markers using world coordinates
255-
rng = np.random.default_rng(9435)
256-
257-
pixels = rng.integers(0, 100, (5, 2))
298+
pixels = np.linspace(0, 100, num=10).reshape(5, 2)
258299
marks_pix = Table(data=pixels, names=['x', 'y'], dtype=('float', 'float'))
259300
marks_world = wcs.pixel_to_world(marks_pix['x'], marks_pix['y'])
260301
marks_coords = SkyCoord(marks_world, unit='degree')
261302
mark_coord_table = Table(data=[marks_coords], names=['coord'])
262303
self.image.add_markers(mark_coord_table, use_skycoord=True)
263304
result = self.image.get_markers()
264305
# Check the x, y positions as long as we are testing things...
265-
np.testing.assert_allclose(result['x'], marks_pix['x'])
306+
# The first test had one entry that was zero, so any check
307+
# based on rtol will will. Added a small atol to make sure
308+
# the test passes.
309+
np.testing.assert_allclose(result['x'], marks_pix['x'], atol=1e-9)
266310
np.testing.assert_allclose(result['y'], marks_pix['y'])
267311
np.testing.assert_allclose(result['coord'].ra.deg,
268312
mark_coord_table['coord'].ra.deg)

0 commit comments

Comments
 (0)