@@ -22,19 +22,19 @@ class ImageViewer:
2222 # These are attributes, not methods. The type annotations are there
2323 # to make sure Protocol knows they are attributes. Python does not
2424 # do any checking at all of these types.
25- click_center : bool = False
26- click_drag : bool = True
25+ _click_center : bool = False
26+ _click_drag : bool = True
2727 scroll_pan : bool = False
2828 image_width : int = 0
2929 image_height : int = 0
3030 zoom_level : float = 1
31- is_marking : bool = False
31+ _is_marking : bool = False
3232 stretch_options : tuple = ("linear" , "log" , "sqrt" )
33- autocut_options : tuple = ("minmax" , "zscale" , "asinh" , "percentile" )
34- cursor : str = ImageViewerInterface .ALLOWED_CURSOR_LOCATIONS [0 ]
33+ autocut_options : tuple = ("minmax" , "zscale" , "asinh" , "percentile" , "histogram" )
34+ _cursor : str = ImageViewerInterface .ALLOWED_CURSOR_LOCATIONS [0 ]
3535 marker : Any = "marker"
36- cuts : Any = (0 , 1 )
37- stretch : str = "linear"
36+ _cuts : str | tuple [ float ] = (0 , 1 )
37+ _stretch : str = "linear"
3838 # viewer: Any
3939
4040 # Allowed locations for cursor display
@@ -59,6 +59,69 @@ class ImageViewer:
5959 _wcs : WCS | None = None
6060 _center : tuple [float , float ] = (0.0 , 0.0 )
6161
62+ # Some properties where we need to control what happens
63+ @property
64+ def is_marking (self ) -> bool :
65+ return self ._is_marking
66+
67+ @property
68+ def click_center (self ) -> bool :
69+ return self ._click_center
70+
71+ @click_center .setter
72+ def click_center (self , value : bool ) -> None :
73+ if self .is_marking :
74+ raise ValueError ("Cannot set click_center while marking is active." )
75+ self ._click_center = value
76+ self ._click_drag = not value
77+
78+ @property
79+ def click_drag (self ) -> bool :
80+ return self ._click_drag
81+ @click_drag .setter
82+ def click_drag (self , value : bool ) -> None :
83+ if self .is_marking :
84+ raise ValueError ("Cannot set click_drag while marking is active." )
85+ self ._click_drag = value
86+ self ._click_center = not value
87+
88+ @property
89+ def stretch (self ) -> str :
90+ return self ._stretch
91+
92+ @stretch .setter
93+ def stretch (self , value : str ) -> None :
94+ if value not in self .stretch_options :
95+ raise ValueError (f"Stretch option { value } is not valid. Must be one of { self .stretch_options } ." )
96+ self ._stretch = value
97+
98+ @property
99+ def cuts (self ) -> tuple :
100+ return self ._cuts
101+
102+ @cuts .setter
103+ def cuts (self , value : tuple ) -> None :
104+ if isinstance (value , str ):
105+ if value not in self .autocut_options :
106+ raise ValueError (f"Cut option { value } is not valid. Must be one of { self .autocut_options } ." )
107+ # A real viewer would calculate the cuts based on the data
108+ self ._cuts = (0 , 1 )
109+ return
110+
111+ if len (value ) != 2 :
112+ raise ValueError ("Cuts must have length 2." )
113+ self ._cuts = value
114+
115+ @property
116+ def cursor (self ) -> str :
117+ return self ._cursor
118+
119+ @cursor .setter
120+ def cursor (self , value : str ) -> None :
121+ if value not in self .ALLOWED_CURSOR_LOCATIONS :
122+ raise ValueError (f"Cursor location { value } is not valid. Must be one of { self .ALLOWED_CURSOR_LOCATIONS } ." )
123+ self ._cursor = value
124+
62125 # The methods, grouped loosely by purpose
63126
64127 # Methods for loading data
@@ -106,7 +169,8 @@ def load_nddata(self, data: NDData) -> None:
106169 The NDData object to load.
107170 """
108171 self ._wcs = data .wcs
109- self .image_height , self .image_width = data .shape
172+ # Not all NDDData objects have a shape, apparently
173+ self .image_height , self .image_width = data .data .shape
110174 # Totally made up number...as currently defined, zoom_level means, esentially, ratio
111175 # of image size to viewer size.
112176 self .zoom_level = 1.0
@@ -128,6 +192,9 @@ def save(self, filename: str | os.PathLike, overwrite: bool = False) -> None:
128192 `False`.
129193 """
130194 p = Path (filename )
195+ if p .exists () and not overwrite :
196+ raise FileExistsError (f"File { filename } already exists. Use overwrite=True to overwrite it." )
197+
131198 p .write_text ("This is a dummy file. The viewer does not save anything." )
132199
133200 # Marker-related methods
@@ -141,13 +208,13 @@ def start_marking(self, marker_name: str | None = None, marker: Any = None) -> N
141208 The name of the marker set to use. If not given, a unique
142209 name will be generated.
143210 """
144- self .is_marking = True
211+ self ._is_marking = True
145212 self ._previous_click_center = self .click_center
146213 self ._previous_click_drag = self .click_drag
147214 self ._previous_marker = self .marker
148215 self ._previous_scroll_pan = self .scroll_pan
149- self .click_center = False
150- self .click_drag = False
216+ self ._click_center = False
217+ self ._click_drag = False
151218 self .scroll_pan = True
152219 self ._interactive_marker_name = marker_name if marker_name else self .DEFAULT_INTERACTIVE_MARKER_NAME
153220 self .marker = marker if marker else self .DEFAULT_INTERACTIVE_MARKER_NAME
@@ -162,7 +229,7 @@ def stop_marking(self, clear_markers: bool = False) -> None:
162229 If `True`, clear the markers that were created during
163230 interactive marking. Default is `False`.
164231 """
165- self .is_marking = False
232+ self ._is_marking = False
166233 self .click_center = self ._previous_click_center
167234 self .click_drag = self ._previous_click_drag
168235 self .scroll_pan = self ._previous_scroll_pan
@@ -197,28 +264,42 @@ def add_markers(self, table: Table, x_colname: str = 'x', y_colname: str = 'y',
197264 The name of the marker set to use. If not given, a unique
198265 name will be generated.
199266 """
267+ try :
268+ coords = table [skycoord_colname ]
269+ except KeyError :
270+ coords = None
200271
201272 if use_skycoord :
202- coords = table [skycoord_colname ]
203273 if self ._wcs is not None :
204274 x , y = self ._wcs .world_to_pixel (coords )
205275 else :
206276 raise ValueError ("WCS is not set. Cannot convert to pixel coordinates." )
207277 else :
208278 x = table [x_colname ]
209279 y = table [y_colname ]
280+
281+ if not coords and self ._wcs is not None :
282+ coords = self ._wcs .pixel_to_world (x , y )
283+
210284 if marker_name in self .RESERVED_MARKER_SET_NAMES :
211285 raise ValueError (f"Marker name { marker_name } not allowed." )
286+
212287 marker_name = marker_name if marker_name else self .DEFAULT_MARKER_NAME
288+
289+ to_add = Table (
290+ dict (
291+ x = x ,
292+ y = y ,
293+ coord = coords if coords else [None ] * len (x ),
294+ )
295+ )
296+ to_add ["marker name" ] = marker_name
297+
213298 if marker_name in self ._markers :
214299 marker_table = self ._markers [marker_name ]
300+ self ._markers [marker_name ] = vstack ([marker_table , to_add ])
215301 else :
216- marker_table = Table (names = ["x" , "y" , "marker name" ],
217- dtype = [float , float , str ])
218-
219- to_add = table [x_colname , y_colname ]
220- to_add [marker_name ] = marker_name
221- self ._markers [marker_name ] = vstack ([marker_table , to_add ])
302+ self ._markers [marker_name ] = to_add
222303
223304 def reset_markers (self ) -> None :
224305 """
@@ -241,10 +322,14 @@ def remove_markers(self, marker_name: str | list[str] | None = None) -> None:
241322 del self ._markers [marker_name ]
242323 elif marker_name == "all" :
243324 self ._markers = {}
325+ else :
326+ raise ValueError (f"Marker name { marker_name } not found." )
244327 elif isinstance (marker_name , list ):
245328 for name in marker_name :
246329 if name in self ._markers :
247330 del self ._markers [name ]
331+ else :
332+ raise ValueError (f"Marker name { name } not found." )
248333
249334 def get_markers (self , x_colname : str = 'x' , y_colname : str = 'y' ,
250335 skycoord_colname : str = 'coord' ,
@@ -278,12 +363,15 @@ def get_markers(self, x_colname: str = 'x', y_colname: str = 'y',
278363 marker_name = self ._markers .keys ()
279364 else :
280365 marker_name = [marker_name ]
366+ elif marker_name is None :
367+ marker_name = [self .DEFAULT_MARKER_NAME ]
281368
282369 to_stack = [self ._markers [name ] for name in marker_name if name in self ._markers ]
283370
284371 result = vstack (to_stack ) if to_stack else Table (names = ["x" , "y" , "coord" , "marker name" ])
372+ result .rename_columns (["x" , "y" , "coord" ], [x_colname , y_colname , skycoord_colname ])
285373
286- return result . rename_columns ([ "x" , "y" , "coord" ], [ x_colname , y_colname , skycoord_colname ])
374+ return result
287375
288376
289377 # Methods that modify the view
@@ -331,7 +419,7 @@ def offset_by(self, dx: float | Quantity, dy: float | Quantity) -> None:
331419 # This is a sky offset
332420 if self ._wcs is not None :
333421 old_center_coord = self ._wcs .pixel_to_world (self ._center [0 ], self ._center [1 ])
334- new_center = old_center_coord .spherical_offsets_byt (dx , dy )
422+ new_center = old_center_coord .spherical_offsets_by (dx , dy )
335423 self .center_on (new_center )
336424 else :
337425 raise ValueError ("WCS is not set. Cannot convert to pixel coordinates." )
@@ -340,7 +428,7 @@ def offset_by(self, dx: float | Quantity, dy: float | Quantity) -> None:
340428 new_center = (self ._center [0 ] + dx .value , self ._center [1 ] + dy .value )
341429 self .center_on (new_center )
342430
343- def zoom (self ) -> None :
431+ def zoom (self , val ) -> None :
344432 """
345433 Zoom in or out by the given factor.
346434
@@ -350,4 +438,4 @@ def zoom(self) -> None:
350438 The zoom level to zoom the image.
351439 See `zoom_level`.
352440 """
353- raise NotImplementedError
441+ self . zoom_level *= val
0 commit comments