@@ -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 ? {
@@ -23,6 +44,33 @@ const DEFAULT_SAMPLE = sampleButtons.length
2344 }
2445 : null ;
2546
47+ function createMarkerIcon ( color ) {
48+ const size = 22 ;
49+ const canvas = document . createElement ( 'canvas' ) ;
50+ canvas . width = size ;
51+ canvas . height = size ;
52+ const context = canvas . getContext ( '2d' ) ;
53+ if ( ! context ) {
54+ return canvas ;
55+ }
56+ const normalizedColor = typeof color === 'string' && color . trim ( ) ? color : '#60A5FA' ;
57+ context . clearRect ( 0 , 0 , size , size ) ;
58+ context . beginPath ( ) ;
59+ context . arc ( size / 2 , size / 2 , ( size / 2 ) - 2 , 0 , Math . PI * 2 ) ;
60+ context . closePath ( ) ;
61+ context . fillStyle = normalizedColor ;
62+ context . fill ( ) ;
63+ context . lineWidth = 2 ;
64+ context . strokeStyle = '#0f172a' ;
65+ context . stroke ( ) ;
66+ context . beginPath ( ) ;
67+ context . arc ( size / 2 , size / 2 , 3 , 0 , Math . PI * 2 ) ;
68+ context . closePath ( ) ;
69+ context . fillStyle = '#0f172a' ;
70+ context . fill ( ) ;
71+ return canvas ;
72+ }
73+
2674function showError ( message ) {
2775 errorBox . textContent = message ;
2876 errorBox . classList . remove ( 'hidden' ) ;
@@ -34,6 +82,10 @@ function clearError() {
3482}
3583
3684function updateStatus ( label , fov ) {
85+ lastStatusSnapshot = {
86+ label : label ?? null ,
87+ fov : Number . isFinite ( fov ) ? fov : null
88+ } ;
3789 const parts = [ ] ;
3890 if ( label ) {
3991 parts . push ( `Source: ${ label } ` ) ;
@@ -45,6 +97,22 @@ function updateStatus(label, fov) {
4597 statusMessage . textContent = parts . join ( ' · ' ) || 'Ready to load a FITS image.' ;
4698}
4799
100+ function setTemporaryStatus ( message ) {
101+ if ( storedStatusText === null ) {
102+ storedStatusText = statusMessage . textContent ;
103+ }
104+ statusMessage . textContent = message ;
105+ }
106+
107+ function restoreStatus ( ) {
108+ if ( storedStatusText !== null ) {
109+ statusMessage . textContent = storedStatusText ;
110+ storedStatusText = null ;
111+ } else {
112+ updateStatus ( lastStatusSnapshot . label , lastStatusSnapshot . fov ) ;
113+ }
114+ }
115+
48116function focusOnImage ( ra , dec , fov ) {
49117 if ( Number . isFinite ( ra ) && Number . isFinite ( dec ) ) {
50118 aladinInstance . gotoRaDec ( ra , dec ) ;
@@ -81,6 +149,175 @@ function deriveLabel(source) {
81149 return 'Custom FITS' ;
82150}
83151
152+ function ensureMarkerLayer ( ) {
153+ if ( ! aladinInstance || markerLayer ) {
154+ return markerLayer ;
155+ }
156+ markerLayer = A . catalog ( {
157+ name : 'Annotations' ,
158+ shape : 'circle' ,
159+ sourceSize : 18 ,
160+ color : '#60A5FA'
161+ } ) ;
162+ aladinInstance . addCatalog ( markerLayer ) ;
163+ return markerLayer ;
164+ }
165+
166+ function updateRemoveMarkerState ( ) {
167+ if ( removeMarkerButton ) {
168+ removeMarkerButton . disabled = placedMarkers . length === 0 ;
169+ }
170+ }
171+
172+ function exitMarkerFlow ( ) {
173+ markerMode = 'idle' ;
174+ if ( addMarkerButton ) {
175+ addMarkerButton . classList . remove ( 'marker-control--active' ) ;
176+ addMarkerButton . disabled = ! aladinInstance ;
177+ }
178+ pendingMarkerPosition = null ;
179+ restoreStatus ( ) ;
180+ }
181+
182+ function openMarkerModal ( ) {
183+ if ( ! markerModal || ! markerForm ) {
184+ return ;
185+ }
186+ markerModal . classList . remove ( 'hidden' ) ;
187+ if ( markerTitleInput ) {
188+ markerTitleInput . focus ( ) ;
189+ }
190+ document . addEventListener ( 'keydown' , handleModalKeydown ) ;
191+ }
192+
193+ function resetMarkerForm ( ) {
194+ if ( ! markerForm ) {
195+ return ;
196+ }
197+ markerForm . reset ( ) ;
198+ if ( markerColorInput ) {
199+ markerColorInput . value = '#60A5FA' ;
200+ }
201+ }
202+
203+ function closeMarkerModal ( ) {
204+ if ( ! markerModal ) {
205+ return ;
206+ }
207+ markerModal . classList . add ( 'hidden' ) ;
208+ document . removeEventListener ( 'keydown' , handleModalKeydown ) ;
209+ resetMarkerForm ( ) ;
210+ exitMarkerFlow ( ) ;
211+ }
212+
213+ function handleModalKeydown ( event ) {
214+ if ( event . key === 'Escape' ) {
215+ event . preventDefault ( ) ;
216+ closeMarkerModal ( ) ;
217+ }
218+ }
219+
220+ function extractCoordinates ( event ) {
221+ const candidates = [ event , event ?. data ] ;
222+ for ( const candidate of candidates ) {
223+ if ( ! candidate ) {
224+ continue ;
225+ }
226+ const ra = Number ( candidate . ra ?? candidate . lon ?? candidate . lng ?? candidate . alpha ) ;
227+ const dec = Number ( candidate . dec ?? candidate . lat ?? candidate . beta ) ;
228+ if ( Number . isFinite ( ra ) && Number . isFinite ( dec ) ) {
229+ return { ra, dec } ;
230+ }
231+ }
232+ return null ;
233+ }
234+
235+ function handleSkyClick ( event ) {
236+ if ( markerMode !== 'armed' ) {
237+ return ;
238+ }
239+ const coordinates = extractCoordinates ( event ) ;
240+ if ( ! coordinates ) {
241+ return ;
242+ }
243+ markerMode = 'pending' ;
244+ pendingMarkerPosition = coordinates ;
245+ if ( addMarkerButton ) {
246+ addMarkerButton . classList . remove ( 'marker-control--active' ) ;
247+ addMarkerButton . disabled = true ;
248+ }
249+ openMarkerModal ( ) ;
250+ }
251+
252+ function startMarkerPlacement ( ) {
253+ if ( ! aladinInstance ) {
254+ return ;
255+ }
256+ ensureMarkerLayer ( ) ;
257+ markerMode = 'armed' ;
258+ if ( addMarkerButton ) {
259+ addMarkerButton . classList . add ( 'marker-control--active' ) ;
260+ addMarkerButton . disabled = false ;
261+ }
262+ setTemporaryStatus ( 'Click on the sky map to choose where the new marker should be placed.' ) ;
263+ }
264+
265+ function handleMarkerSubmission ( event ) {
266+ event . preventDefault ( ) ;
267+ if ( ! pendingMarkerPosition || ! markerLayer ) {
268+ closeMarkerModal ( ) ;
269+ return ;
270+ }
271+ const title = markerTitleInput ?. value . trim ( ) || `Marker ${ placedMarkers . length + 1 } ` ;
272+ const description = markerDescriptionInput ?. value . trim ( ) || '' ;
273+ const color = markerColorInput ?. value || '#60A5FA' ;
274+ const markerOptions = {
275+ popupTitle : title ,
276+ popupDesc : description
277+ } ;
278+ try {
279+ const marker = A . marker ( pendingMarkerPosition . ra , pendingMarkerPosition . dec , markerOptions ) ;
280+ marker . useMarkerDefaultIcon = false ;
281+ if ( typeof marker . setImage === 'function' ) {
282+ marker . setImage ( createMarkerIcon ( color ) ) ;
283+ }
284+ marker . color = color ;
285+ markerLayer . addSources ( [ marker ] ) ;
286+ placedMarkers . push ( marker ) ;
287+ updateRemoveMarkerState ( ) ;
288+ } catch ( error ) {
289+ console . error ( 'Unable to add marker' , error ) ;
290+ showError ( 'We could not create the marker. Please try again.' ) ;
291+ }
292+ closeMarkerModal ( ) ;
293+ }
294+
295+ function removeLatestMarker ( ) {
296+ if ( ! markerLayer || placedMarkers . length === 0 ) {
297+ return ;
298+ }
299+ const marker = placedMarkers . pop ( ) ;
300+ try {
301+ if ( typeof markerLayer . remove === 'function' ) {
302+ markerLayer . remove ( marker ) ;
303+ } else if ( typeof markerLayer . removeSources === 'function' ) {
304+ markerLayer . removeSources ( [ marker ] ) ;
305+ } else if ( typeof markerLayer . removeSource === 'function' ) {
306+ markerLayer . removeSource ( marker ) ;
307+ } else if ( typeof markerLayer . removeAll === 'function' ) {
308+ markerLayer . removeAll ( ) ;
309+ placedMarkers . length = 0 ;
310+ }
311+ } catch ( error ) {
312+ console . warn ( 'Unable to remove marker individually, clearing all markers' , error ) ;
313+ if ( typeof markerLayer . removeAll === 'function' ) {
314+ markerLayer . removeAll ( ) ;
315+ placedMarkers . length = 0 ;
316+ }
317+ }
318+ updateRemoveMarkerState ( ) ;
319+ }
320+
84321function loadFits ( source , options = { } ) {
85322 if ( ! aladinInstance || ! source ) {
86323 return ;
@@ -198,10 +435,14 @@ async function initialiseViewer() {
198435 target : 'M 31'
199436 } ) ;
200437
201- var marker1 = A . marker ( 0 , 0 , { popupTitle : "Test" , popupDesc : "TEST" } ) ;
202- var markerLayer = A . catalog ( ) ;
203- aladinInstance . addCatalog ( markerLayer ) ;
204- markerLayer . addSources ( [ marker1 ] ) ;
438+ ensureMarkerLayer ( ) ;
439+ if ( addMarkerButton ) {
440+ addMarkerButton . disabled = false ;
441+ }
442+ updateRemoveMarkerState ( ) ;
443+ if ( typeof aladinInstance . on === 'function' ) {
444+ aladinInstance . on ( 'click' , handleSkyClick ) ;
445+ }
205446 } ) ;
206447
207448 initialiseSampleButtons ( ) ;
@@ -229,9 +470,35 @@ urlInput.addEventListener('keydown', (event) => {
229470 }
230471} ) ;
231472
232- openComparisonButton = document . getElementById ( "open-comparison" ) ;
233- openComparisonButton . addEventListener ( 'click' , ( ) => {
234- console . log ( aladinInstance . getBaseImageLayer ( ) ) ;
235- } ) ;
473+ if ( addMarkerButton ) {
474+ addMarkerButton . addEventListener ( 'click' , ( ) => {
475+ if ( ! aladinInstance ) {
476+ showError ( 'The FITS viewer is still initialising. Please wait a moment and try again.' ) ;
477+ return ;
478+ }
479+ if ( markerMode === 'armed' ) {
480+ exitMarkerFlow ( ) ;
481+ return ;
482+ }
483+ startMarkerPlacement ( ) ;
484+ } ) ;
485+ }
486+
487+ if ( markerCancelButton ) {
488+ markerCancelButton . addEventListener ( 'click' , ( event ) => {
489+ event . preventDefault ( ) ;
490+ closeMarkerModal ( ) ;
491+ } ) ;
492+ }
493+
494+ if ( markerForm ) {
495+ markerForm . addEventListener ( 'submit' , handleMarkerSubmission ) ;
496+ }
497+
498+ if ( removeMarkerButton ) {
499+ removeMarkerButton . addEventListener ( 'click' , ( ) => {
500+ removeLatestMarker ( ) ;
501+ } ) ;
502+ }
236503
237504initialiseViewer ( ) ;
0 commit comments