@@ -11,9 +11,30 @@ const fileInput = document.getElementById('fits-file');
1111const urlInput = document . getElementById ( 'fits-url' ) ;
1212const loadUrlButton = document . getElementById ( 'load-url' ) ;
1313const sampleButtons = Array . from ( document . querySelectorAll ( '[data-fits-url]' ) ) ;
14+ const addMarkerButton = document . getElementById ( 'add-marker' ) ;
15+ const removeMarkerButton = document . getElementById ( 'remove-marker' ) ;
16+ const markerModal = document . getElementById ( 'marker-modal' ) ;
17+ const markerForm = markerModal ? markerModal . querySelector ( 'form' ) : null ;
18+ const markerCancelButton = markerModal ? markerModal . querySelector ( '[data-action="cancel"]' ) : null ;
19+ const markerTitleInput = document . getElementById ( 'marker-title' ) ;
20+ const markerDescriptionInput = document . getElementById ( 'marker-description' ) ;
21+ const markerColorInput = document . getElementById ( 'marker-color' ) ;
1422
1523let aladinInstance = null ;
1624let currentRequestId = 0 ;
25+ let markerLayer = null ;
26+ let markerMode = 'idle' ;
27+ let pendingMarkerPosition = null ;
28+ let storedStatusText = null ;
29+ const placedMarkers = [ ] ;
30+ let lastStatusSnapshot = { label : null , fov : null } ;
31+
32+ if ( addMarkerButton ) {
33+ addMarkerButton . disabled = true ;
34+ }
35+ if ( removeMarkerButton ) {
36+ removeMarkerButton . disabled = true ;
37+ }
1738
1839const DEFAULT_SAMPLE = sampleButtons . length
1940 ? {
@@ -34,6 +55,10 @@ function clearError() {
3455}
3556
3657function updateStatus ( label , fov ) {
58+ lastStatusSnapshot = {
59+ label : label ?? null ,
60+ fov : Number . isFinite ( fov ) ? fov : null
61+ } ;
3762 const parts = [ ] ;
3863 if ( label ) {
3964 parts . push ( `Source: ${ label } ` ) ;
@@ -45,6 +70,22 @@ function updateStatus(label, fov) {
4570 statusMessage . textContent = parts . join ( ' · ' ) || 'Ready to load a FITS image.' ;
4671}
4772
73+ function setTemporaryStatus ( message ) {
74+ if ( storedStatusText === null ) {
75+ storedStatusText = statusMessage . textContent ;
76+ }
77+ statusMessage . textContent = message ;
78+ }
79+
80+ function restoreStatus ( ) {
81+ if ( storedStatusText !== null ) {
82+ statusMessage . textContent = storedStatusText ;
83+ storedStatusText = null ;
84+ } else {
85+ updateStatus ( lastStatusSnapshot . label , lastStatusSnapshot . fov ) ;
86+ }
87+ }
88+
4889function focusOnImage ( ra , dec , fov ) {
4990 if ( Number . isFinite ( ra ) && Number . isFinite ( dec ) ) {
5091 aladinInstance . gotoRaDec ( ra , dec ) ;
@@ -81,6 +122,173 @@ function deriveLabel(source) {
81122 return 'Custom FITS' ;
82123}
83124
125+ function ensureMarkerLayer ( ) {
126+ if ( ! aladinInstance || markerLayer ) {
127+ return markerLayer ;
128+ }
129+ markerLayer = A . catalog ( {
130+ name : 'Annotations' ,
131+ shape : 'circle' ,
132+ sourceSize : 18 ,
133+ color : '#60A5FA'
134+ } ) ;
135+ aladinInstance . addCatalog ( markerLayer ) ;
136+ return markerLayer ;
137+ }
138+
139+ function updateRemoveMarkerState ( ) {
140+ if ( removeMarkerButton ) {
141+ removeMarkerButton . disabled = placedMarkers . length === 0 ;
142+ }
143+ }
144+
145+ function exitMarkerFlow ( ) {
146+ markerMode = 'idle' ;
147+ if ( addMarkerButton ) {
148+ addMarkerButton . classList . remove ( 'marker-control--active' ) ;
149+ addMarkerButton . disabled = ! aladinInstance ;
150+ }
151+ pendingMarkerPosition = null ;
152+ restoreStatus ( ) ;
153+ }
154+
155+ function openMarkerModal ( ) {
156+ if ( ! markerModal || ! markerForm ) {
157+ return ;
158+ }
159+ markerModal . classList . remove ( 'hidden' ) ;
160+ if ( markerTitleInput ) {
161+ markerTitleInput . focus ( ) ;
162+ }
163+ document . addEventListener ( 'keydown' , handleModalKeydown ) ;
164+ }
165+
166+ function resetMarkerForm ( ) {
167+ if ( ! markerForm ) {
168+ return ;
169+ }
170+ markerForm . reset ( ) ;
171+ if ( markerColorInput ) {
172+ markerColorInput . value = '#60A5FA' ;
173+ }
174+ }
175+
176+ function closeMarkerModal ( ) {
177+ if ( ! markerModal ) {
178+ return ;
179+ }
180+ markerModal . classList . add ( 'hidden' ) ;
181+ document . removeEventListener ( 'keydown' , handleModalKeydown ) ;
182+ resetMarkerForm ( ) ;
183+ exitMarkerFlow ( ) ;
184+ }
185+
186+ function handleModalKeydown ( event ) {
187+ if ( event . key === 'Escape' ) {
188+ event . preventDefault ( ) ;
189+ closeMarkerModal ( ) ;
190+ }
191+ }
192+
193+ function extractCoordinates ( event ) {
194+ const candidates = [ event , event ?. data ] ;
195+ for ( const candidate of candidates ) {
196+ if ( ! candidate ) {
197+ continue ;
198+ }
199+ const ra = Number ( candidate . ra ?? candidate . lon ?? candidate . lng ?? candidate . alpha ) ;
200+ const dec = Number ( candidate . dec ?? candidate . lat ?? candidate . beta ) ;
201+ if ( Number . isFinite ( ra ) && Number . isFinite ( dec ) ) {
202+ return { ra, dec } ;
203+ }
204+ }
205+ return null ;
206+ }
207+
208+ function handleSkyClick ( event ) {
209+ if ( markerMode !== 'armed' ) {
210+ return ;
211+ }
212+ const coordinates = extractCoordinates ( event ) ;
213+ if ( ! coordinates ) {
214+ return ;
215+ }
216+ markerMode = 'pending' ;
217+ pendingMarkerPosition = coordinates ;
218+ if ( addMarkerButton ) {
219+ addMarkerButton . classList . remove ( 'marker-control--active' ) ;
220+ addMarkerButton . disabled = true ;
221+ }
222+ openMarkerModal ( ) ;
223+ }
224+
225+ function startMarkerPlacement ( ) {
226+ if ( ! aladinInstance ) {
227+ return ;
228+ }
229+ ensureMarkerLayer ( ) ;
230+ markerMode = 'armed' ;
231+ if ( addMarkerButton ) {
232+ addMarkerButton . classList . add ( 'marker-control--active' ) ;
233+ addMarkerButton . disabled = false ;
234+ }
235+ setTemporaryStatus ( 'Click on the sky map to choose where the new marker should be placed.' ) ;
236+ }
237+
238+ function handleMarkerSubmission ( event ) {
239+ event . preventDefault ( ) ;
240+ if ( ! pendingMarkerPosition || ! markerLayer ) {
241+ closeMarkerModal ( ) ;
242+ return ;
243+ }
244+ const title = markerTitleInput ?. value . trim ( ) || `Marker ${ placedMarkers . length + 1 } ` ;
245+ const description = markerDescriptionInput ?. value . trim ( ) || '' ;
246+ const color = markerColorInput ?. value || '#60A5FA' ;
247+ const markerOptions = {
248+ popupTitle : title ,
249+ popupDesc : description
250+ } ;
251+ if ( color ) {
252+ markerOptions . color = color ;
253+ }
254+ try {
255+ const marker = A . marker ( pendingMarkerPosition . ra , pendingMarkerPosition . dec , markerOptions ) ;
256+ markerLayer . addSources ( [ marker ] ) ;
257+ placedMarkers . push ( marker ) ;
258+ updateRemoveMarkerState ( ) ;
259+ } catch ( error ) {
260+ console . error ( 'Unable to add marker' , error ) ;
261+ showError ( 'We could not create the marker. Please try again.' ) ;
262+ }
263+ closeMarkerModal ( ) ;
264+ }
265+
266+ function removeLatestMarker ( ) {
267+ if ( ! markerLayer || placedMarkers . length === 0 ) {
268+ return ;
269+ }
270+ const marker = placedMarkers . pop ( ) ;
271+ try {
272+ if ( typeof markerLayer . remove === 'function' ) {
273+ markerLayer . remove ( marker ) ;
274+ } else if ( typeof markerLayer . removeSources === 'function' ) {
275+ markerLayer . removeSources ( [ marker ] ) ;
276+ } else if ( typeof markerLayer . removeSource === 'function' ) {
277+ markerLayer . removeSource ( marker ) ;
278+ } else if ( typeof markerLayer . removeAll === 'function' ) {
279+ markerLayer . removeAll ( ) ;
280+ placedMarkers . length = 0 ;
281+ }
282+ } catch ( error ) {
283+ console . warn ( 'Unable to remove marker individually, clearing all markers' , error ) ;
284+ if ( typeof markerLayer . removeAll === 'function' ) {
285+ markerLayer . removeAll ( ) ;
286+ placedMarkers . length = 0 ;
287+ }
288+ }
289+ updateRemoveMarkerState ( ) ;
290+ }
291+
84292function loadFits ( source , options = { } ) {
85293 if ( ! aladinInstance || ! source ) {
86294 return ;
@@ -198,10 +406,14 @@ async function initialiseViewer() {
198406 target : 'M 31'
199407 } ) ;
200408
201- var marker1 = A . marker ( 0 , 0 , { popupTitle : "Test" , popupDesc : "TEST" } ) ;
202- var markerLayer = A . catalog ( ) ;
203- aladinInstance . addCatalog ( markerLayer ) ;
204- markerLayer . addSources ( [ marker1 ] ) ;
409+ ensureMarkerLayer ( ) ;
410+ if ( addMarkerButton ) {
411+ addMarkerButton . disabled = false ;
412+ }
413+ updateRemoveMarkerState ( ) ;
414+ if ( typeof aladinInstance . on === 'function' ) {
415+ aladinInstance . on ( 'click' , handleSkyClick ) ;
416+ }
205417 } ) ;
206418
207419 initialiseSampleButtons ( ) ;
@@ -234,4 +446,35 @@ openComparisonButton.addEventListener('click', () => {
234446 console . log ( aladinInstance . getBaseImageLayer ( ) ) ;
235447} ) ;
236448
449+ if ( addMarkerButton ) {
450+ addMarkerButton . addEventListener ( 'click' , ( ) => {
451+ if ( ! aladinInstance ) {
452+ showError ( 'The FITS viewer is still initialising. Please wait a moment and try again.' ) ;
453+ return ;
454+ }
455+ if ( markerMode === 'armed' ) {
456+ exitMarkerFlow ( ) ;
457+ return ;
458+ }
459+ startMarkerPlacement ( ) ;
460+ } ) ;
461+ }
462+
463+ if ( markerCancelButton ) {
464+ markerCancelButton . addEventListener ( 'click' , ( event ) => {
465+ event . preventDefault ( ) ;
466+ closeMarkerModal ( ) ;
467+ } ) ;
468+ }
469+
470+ if ( markerForm ) {
471+ markerForm . addEventListener ( 'submit' , handleMarkerSubmission ) ;
472+ }
473+
474+ if ( removeMarkerButton ) {
475+ removeMarkerButton . addEventListener ( 'click' , ( ) => {
476+ removeLatestMarker ( ) ;
477+ } ) ;
478+ }
479+
237480initialiseViewer ( ) ;
0 commit comments