11/**
22 * SPDX-FileCopyrightText: Copyright (C) 2026, CNES (Rollin Gimenez)
33 * SPDX-License-Identifier: Apache-2.0
4- *
4+ *
55 * map.js — Aladin Lite v3 sky map
6- * Click on the sky → dispatches "sky:select" with { ra, dec }
76 */
87
98import { API } from './websocket.js' ;
109
11- let aladin = null ;
12- let markerLayer = null ;
13- let tilingOverlay = null ;
10+ let aladin = null ;
11+ let markerLayer = null ;
12+ let tilingOverlay = null ;
13+ let circleOverlay = null ;
14+ let previewOverlay = null ;
15+ let firstClick = null ;
16+
1417
1518export async function initMap ( containerId ) {
1619 await loadAladinScript ( ) ;
@@ -22,26 +25,83 @@ export async function initMap(containerId) {
2225 target : '0 0' ,
2326 cooFrame : 'ICRSd' ,
2427 showReticle : false ,
28+ showProjectionControl : false ,
2529 showZoomControl : true ,
2630 showFullscreenControl : true ,
2731 showLayersControl : true ,
28- showGotoControl : true ,
32+ showGotoControl : false ,
2933 showShareControl : true ,
30- showCooLocation : true ,
34+ showCooLocation : false ,
3135 } ) ;
32-
36+
3337 markerLayer = A . catalog ( { shape : 'circle' , color : '#4ec9b0' , sourceSize : 12 } ) ;
3438 aladin . addCatalog ( markerLayer ) ;
3539
40+ circleOverlay = A . graphicOverlay ( { color : '#8066be' , lineWidth : 2 } ) ;
41+ previewOverlay = A . graphicOverlay ( { color : '#8066be' , lineWidth : 1 , lineDash : [ 4 , 4 ] } ) ;
42+ aladin . addOverlay ( circleOverlay ) ;
43+ aladin . addOverlay ( previewOverlay ) ;
44+
45+ // Clic
3646 aladin . on ( 'click' , ( raOrObj , decArg ) => {
47+
3748 const ra = ( raOrObj !== null && typeof raOrObj === 'object' ) ? raOrObj . ra : raOrObj ;
3849 const dec = ( raOrObj !== null && typeof raOrObj === 'object' ) ? raOrObj . dec : decArg ;
3950 if ( ra == null || dec == null ) return ;
40- placeMarker ( ra , dec ) ;
41- document . dispatchEvent ( new CustomEvent ( 'sky:select' , { detail : { ra, dec } } ) ) ;
51+
52+ if ( ! firstClick ) {
53+ // 1st clic → center
54+ firstClick = { ra, dec } ;
55+ placeMarker ( ra , dec ) ;
56+ circleOverlay . removeAll ( ) ;
57+ previewOverlay . removeAll ( ) ;
58+ document . dispatchEvent ( new CustomEvent ( 'sky:select' , { detail : { ra, dec } } ) ) ;
59+ } else {
60+ // 2nd clic → radius
61+ const radius = angularDistance ( firstClick . ra , firstClick . dec , ra , dec ) ;
62+ previewOverlay . removeAll ( ) ;
63+ drawCircleOn ( circleOverlay , firstClick . ra , firstClick . dec , radius ) ;
64+ document . dispatchEvent ( new CustomEvent ( 'sky:region' , {
65+ detail : { ra : firstClick . ra , dec : firstClick . dec , radius }
66+ } ) ) ;
67+ firstClick = null ;
68+ }
69+ } ) ;
70+
71+ // Mousemove → circle preview
72+ const aladinDiv = document . getElementById ( containerId ) ;
73+ aladinDiv . addEventListener ( 'mousemove' , e => {
74+ if ( ! firstClick || ! aladin . pix2world ) return ;
75+
76+ const rect = aladinDiv . getBoundingClientRect ( ) ;
77+ const x = e . clientX - rect . left ;
78+ const y = e . clientY - rect . top ;
79+
80+ // pix2world returns [ra, dec] in degrees
81+ const skyCoords = aladin . pix2world ( x , y ) ;
82+ if ( ! skyCoords || skyCoords [ 0 ] == null ) return ;
83+
84+ const [ raMouse , decMouse ] = skyCoords ;
85+ const radius = angularDistance ( firstClick . ra , firstClick . dec , raMouse , decMouse ) ;
86+
87+ previewOverlay . removeAll ( ) ;
88+ if ( radius > 0 ) {
89+ drawCircleOn ( previewOverlay , firstClick . ra , firstClick . dec , radius ) ;
90+ }
91+ } ) ;
92+
93+ // cancel with escape
94+ document . addEventListener ( 'keydown' , e => {
95+ if ( e . key === 'Escape' && firstClick ) {
96+ firstClick = null ;
97+ previewOverlay . removeAll ( ) ;
98+ markerLayer . clear ( ) ;
99+ }
42100 } ) ;
43101}
44102
103+ // Helpers
104+
45105function placeMarker ( ra , dec ) {
46106 markerLayer . clear ( ) ;
47107 markerLayer . addSources ( [ A . source ( ra , dec ) ] ) ;
@@ -51,15 +111,44 @@ export function goTo(ra, dec) {
51111 if ( ! aladin ) return ;
52112 aladin . gotoRaDec ( ra , dec ) ;
53113 placeMarker ( ra , dec ) ;
114+ firstClick = null ;
115+ circleOverlay ?. removeAll ( ) ;
116+ previewOverlay ?. removeAll ( ) ;
54117}
55118
56- /**
57- * Load tiling polygons from the backend and display them as an overlay on the map.
58- */
119+ function drawCircleOn ( overlay , ra , dec , radiusDeg , steps = 64 ) {
120+ const points = [ ] ;
121+ const decRad = dec * Math . PI / 180 ;
122+ for ( let i = 0 ; i < steps ; i ++ ) {
123+ const angle = ( i / steps ) * 2 * Math . PI ;
124+ const dRa = ( radiusDeg * Math . cos ( angle ) ) / Math . cos ( decRad ) ;
125+ const dDec = radiusDeg * Math . sin ( angle ) ;
126+ points . push ( [ ra + dRa , dec + dDec ] ) ;
127+ }
128+ overlay . removeAll ( ) ;
129+ overlay . add ( A . polygon ( points ) ) ;
130+ }
131+
132+ export function drawCircle ( ra , dec , radius ) {
133+ if ( ! aladin || ! circleOverlay ) return ;
134+ circleOverlay . removeAll ( ) ;
135+ if ( radius == null ) return ;
136+ drawCircleOn ( circleOverlay , ra , dec , radius ) ;
137+ }
138+
139+ function angularDistance ( ra1 , dec1 , ra2 , dec2 ) {
140+ const toRad = d => d * Math . PI / 180 ;
141+ const cos =
142+ Math . sin ( toRad ( dec1 ) ) * Math . sin ( toRad ( dec2 ) ) +
143+ Math . cos ( toRad ( dec1 ) ) * Math . cos ( toRad ( dec2 ) ) * Math . cos ( toRad ( ra1 - ra2 ) ) ;
144+ return Math . acos ( Math . min ( 1 , Math . max ( - 1 , cos ) ) ) * 180 / Math . PI ;
145+ }
146+
147+ // Tiling
148+
59149export async function loadTiling ( filename ) {
60150 if ( ! aladin || ! filename ) return ;
61151
62- // Remove existing tiling overlay if any
63152 if ( tilingOverlay ) {
64153 aladin . removeOverlay ( tilingOverlay ) ;
65154 tilingOverlay = null ;
@@ -79,14 +168,13 @@ export async function loadTiling(filename) {
79168 aladin . addOverlay ( tilingOverlay ) ;
80169
81170 for ( const tile of data . tiles ) {
82- // coords GeoJSON : [[ra, dec], ...] - Aladin [[ra, dec], ...]
83- const footprint = A . polygon ( tile . coords . map ( ( [ ra , dec ] ) => [ ra , dec ] ) ) ;
84- tilingOverlay . add ( footprint ) ;
171+ tilingOverlay . add ( A . polygon ( tile . coords . map ( ( [ ra , dec ] ) => [ ra , dec ] ) ) ) ;
85172 }
86-
87173 console . log ( `Loaded ${ data . tiles . length } tile polygons` ) ;
88174}
89175
176+ // Aladin loader
177+
90178function loadAladinScript ( ) {
91179 return new Promise ( ( resolve , reject ) => {
92180 if ( window . A ) { resolve ( ) ; return ; }
0 commit comments