Skip to content

Commit d1f7361

Browse files
committed
Add marker; choosing colour, title, and description.
1 parent a93cd13 commit d1f7361

2 files changed

Lines changed: 349 additions & 4 deletions

File tree

web/aladin.html

Lines changed: 225 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<input id="fits-url" type="url" placeholder="https://data.nasa.gov/sample.fits" aria-label="FITS file URL" />
2424
<button type="button" id="load-url">Load URL</button>
2525
</div>
26+
<button type="button" id="add-marker" class="marker-control">Add marker</button>
2627
</div>
2728
<div class="samples">
2829
<span class="samples-label">Try a sample dataset:</span>
@@ -58,6 +59,28 @@
5859
</p>
5960
<p class="notice hidden" id="aladin-error" role="alert"></p>
6061
</aside>
62+
<div id="marker-modal" class="modal hidden" role="dialog" aria-modal="true" aria-labelledby="marker-modal-title">
63+
<form class="modal__content">
64+
<h2 id="marker-modal-title">Add marker</h2>
65+
<p class="modal__hint">Customize the marker details before saving it to the sky map.</p>
66+
<label for="marker-title">
67+
<span>Title</span>
68+
<input id="marker-title" name="marker-title" type="text" autocomplete="off" />
69+
</label>
70+
<label for="marker-description">
71+
<span>Description</span>
72+
<textarea id="marker-description" name="marker-description" rows="3"></textarea>
73+
</label>
74+
<label for="marker-color" class="modal__color-picker">
75+
<span>Marker color</span>
76+
<input id="marker-color" name="marker-color" type="color" value="#60A5FA" />
77+
</label>
78+
<div class="modal__actions">
79+
<button type="button" class="modal__button modal__button--secondary" data-action="cancel">Cancel</button>
80+
<button type="submit" class="modal__button modal__button--primary">Save marker</button>
81+
</div>
82+
</form>
83+
</div>
6184
<script>
6285
const { A } = window;
6386
if (!A) {
@@ -69,10 +92,23 @@
6992
const fileInput = document.getElementById('fits-file');
7093
const urlInput = document.getElementById('fits-url');
7194
const loadUrlButton = document.getElementById('load-url');
95+
const addMarkerButton = document.getElementById('add-marker');
7296
const sampleButtons = Array.from(document.querySelectorAll('[data-fits-url]'));
97+
const markerModal = document.getElementById('marker-modal');
98+
const markerForm = markerModal ? markerModal.querySelector('form') : null;
99+
const markerTitleInput = markerModal ? markerModal.querySelector('#marker-title') : null;
100+
const markerDescriptionInput = markerModal ? markerModal.querySelector('#marker-description') : null;
101+
const markerColorInput = markerModal ? markerModal.querySelector('#marker-color') : null;
102+
const markerCancelButton = markerModal ? markerModal.querySelector('[data-action="cancel"]') : null;
73103

74104
let aladinInstance = null;
75105
let currentRequestId = 0;
106+
const markerLayersByColor = new Map();
107+
let isAddingMarker = false;
108+
let storedStatusText = statusMessage.textContent;
109+
let resolveMarkerDialog = null;
110+
const MARKER_DEFAULT_COLOR = '#60A5FA';
111+
let lastSelectedMarkerColor = MARKER_DEFAULT_COLOR;
76112

77113
const DEFAULT_SAMPLE = sampleButtons.length
78114
? {
@@ -102,6 +138,7 @@
102138
parts.push(`FoV ≈ ${span.toFixed(2)}°`);
103139
}
104140
statusMessage.textContent = parts.join(' · ') || 'Ready to load a FITS image.';
141+
storedStatusText = statusMessage.textContent;
105142
}
106143

107144
function focusOnImage(ra, dec, fov) {
@@ -140,6 +177,88 @@
140177
return 'Custom FITS';
141178
}
142179

180+
function normaliseHexColor(input) {
181+
if (typeof input !== 'string') {
182+
return null;
183+
}
184+
const match = input.trim().match(/^#([0-9a-f]{6})$/i);
185+
return match ? `#${match[1].toUpperCase()}` : null;
186+
}
187+
188+
function ensureMarkerLayer(color) {
189+
if (!aladinInstance) {
190+
return null;
191+
}
192+
const normalizedColor = normaliseHexColor(color) || MARKER_DEFAULT_COLOR;
193+
const existing = markerLayersByColor.get(normalizedColor);
194+
if (existing) {
195+
return existing;
196+
}
197+
const catalog = A.catalog({
198+
name: `Markers ${normalizedColor}`,
199+
shape: 'marker',
200+
color: normalizedColor
201+
});
202+
markerLayersByColor.set(normalizedColor, catalog);
203+
aladinInstance.addCatalog(catalog);
204+
return catalog;
205+
}
206+
207+
function isMarkerDialogOpen() {
208+
return Boolean(markerModal && !markerModal.classList.contains('hidden'));
209+
}
210+
211+
function closeMarkerDialog(result) {
212+
if (!markerModal || !markerForm) {
213+
if (typeof resolveMarkerDialog === 'function') {
214+
resolveMarkerDialog(result);
215+
resolveMarkerDialog = null;
216+
}
217+
return;
218+
}
219+
markerModal.classList.add('hidden');
220+
markerModal.setAttribute('aria-hidden', 'true');
221+
markerForm.reset();
222+
if (markerColorInput) {
223+
markerColorInput.value = lastSelectedMarkerColor || MARKER_DEFAULT_COLOR;
224+
}
225+
if (typeof resolveMarkerDialog === 'function') {
226+
resolveMarkerDialog(result);
227+
resolveMarkerDialog = null;
228+
}
229+
}
230+
231+
function openMarkerDialog(defaultTitle, defaultDescription, defaultColor) {
232+
const resolvedDefaultColor = normaliseHexColor(defaultColor) || MARKER_DEFAULT_COLOR;
233+
if (!markerModal || !markerForm || !markerTitleInput || !markerDescriptionInput || !markerColorInput) {
234+
if (typeof resolveMarkerDialog === 'function') {
235+
resolveMarkerDialog(null);
236+
resolveMarkerDialog = null;
237+
}
238+
return Promise.resolve({
239+
title: defaultTitle,
240+
description: defaultDescription,
241+
color: resolvedDefaultColor
242+
});
243+
}
244+
if (typeof resolveMarkerDialog === 'function') {
245+
resolveMarkerDialog(null);
246+
resolveMarkerDialog = null;
247+
}
248+
markerTitleInput.value = defaultTitle;
249+
markerDescriptionInput.value = defaultDescription;
250+
markerColorInput.value = resolvedDefaultColor;
251+
markerModal.classList.remove('hidden');
252+
markerModal.setAttribute('aria-hidden', 'false');
253+
return new Promise((resolve) => {
254+
resolveMarkerDialog = resolve;
255+
window.requestAnimationFrame(() => {
256+
markerTitleInput.focus();
257+
markerTitleInput.select();
258+
});
259+
});
260+
}
261+
143262
function loadFits(source, options = {}) {
144263
if (!aladinInstance || !source) {
145264
return;
@@ -241,6 +360,98 @@
241360
});
242361
}
243362

363+
if (markerForm && markerTitleInput && markerDescriptionInput && markerColorInput) {
364+
markerForm.addEventListener('submit', (event) => {
365+
event.preventDefault();
366+
const chosenColor = normaliseHexColor(markerColorInput.value) || MARKER_DEFAULT_COLOR;
367+
closeMarkerDialog({
368+
title: markerTitleInput.value,
369+
description: markerDescriptionInput.value,
370+
color: chosenColor
371+
});
372+
});
373+
}
374+
375+
if (markerCancelButton) {
376+
markerCancelButton.addEventListener('click', () => {
377+
closeMarkerDialog(null);
378+
});
379+
}
380+
381+
if (markerModal) {
382+
markerModal.addEventListener('click', (event) => {
383+
if (event.target === markerModal) {
384+
closeMarkerDialog(null);
385+
}
386+
});
387+
}
388+
389+
window.addEventListener('keydown', (event) => {
390+
if (event.key === 'Escape' && isMarkerDialogOpen()) {
391+
event.preventDefault();
392+
closeMarkerDialog(null);
393+
}
394+
});
395+
396+
function toggleMarkerMode(active) {
397+
if (!aladinInstance) {
398+
showError('The viewer is not ready yet. Please wait a moment and try again.');
399+
return;
400+
}
401+
isAddingMarker = active;
402+
addMarkerButton.classList.toggle('marker-control--active', isAddingMarker);
403+
if (isAddingMarker) {
404+
storedStatusText = statusMessage.textContent;
405+
statusMessage.textContent = 'Click anywhere on the map to place a marker. Press Esc to cancel.';
406+
} else {
407+
statusMessage.textContent = storedStatusText;
408+
}
409+
if (!isAddingMarker) {
410+
window.removeEventListener('keydown', cancelMarkerModeOnEscape);
411+
} else {
412+
window.addEventListener('keydown', cancelMarkerModeOnEscape);
413+
}
414+
}
415+
416+
function cancelMarkerModeOnEscape(event) {
417+
if (event.key === 'Escape') {
418+
if (isMarkerDialogOpen()) {
419+
return;
420+
}
421+
toggleMarkerMode(false);
422+
}
423+
}
424+
425+
async function createMarkerAt(ra, dec) {
426+
const defaultTitle = `Marker @ RA ${ra.toFixed(3)}, Dec ${dec.toFixed(3)}`;
427+
const defaultDescription = `Coordinates: RA ${ra.toFixed(3)}, Dec ${dec.toFixed(3)}`;
428+
const dialogResult = await openMarkerDialog(defaultTitle, defaultDescription, lastSelectedMarkerColor);
429+
if (!dialogResult) {
430+
return false;
431+
}
432+
const trimmedTitle = (dialogResult.title || '').trim();
433+
const trimmedDescription = (dialogResult.description || '').trim();
434+
const validColor = normaliseHexColor(dialogResult.color) || MARKER_DEFAULT_COLOR;
435+
lastSelectedMarkerColor = validColor;
436+
const targetLayer = ensureMarkerLayer(validColor);
437+
if (!targetLayer) {
438+
showError('Unable to access the marker layer. Please try again in a moment.');
439+
return false;
440+
}
441+
const markerOptions = {
442+
popupTitle: trimmedTitle || defaultTitle,
443+
popupDesc: trimmedDescription || defaultDescription,
444+
color: validColor
445+
};
446+
const marker = A.marker(ra, dec, markerOptions);
447+
targetLayer.addSources([marker]);
448+
aladinInstance.gotoRaDec(ra, dec);
449+
const confirmation = `${markerOptions.popupTitle} saved at RA ${ra.toFixed(3)}, Dec ${dec.toFixed(3)}`;
450+
statusMessage.textContent = confirmation;
451+
storedStatusText = confirmation;
452+
return true;
453+
}
454+
244455
async function initialiseViewer() {
245456
if (!A) {
246457
showError('Aladin Lite could not be loaded. Please check your connection and refresh the page.');
@@ -257,10 +468,17 @@
257468
target: 'M 31'
258469
});
259470

260-
var marker1 = A.marker(0, 0, {popupTitle: "Test", popupDesc: "TEST"});
261-
var markerLayer = A.catalog();
262-
aladinInstance.addCatalog(markerLayer);
263-
markerLayer.addSources([marker1]);
471+
ensureMarkerLayer(MARKER_DEFAULT_COLOR);
472+
aladinInstance.on('click', async (point) => {
473+
if (!isAddingMarker || !point) {
474+
return;
475+
}
476+
const { ra, dec } = point;
477+
if (Number.isFinite(ra) && Number.isFinite(dec)) {
478+
await createMarkerAt(ra, dec);
479+
}
480+
toggleMarkerMode(false);
481+
});
264482
});
265483

266484
initialiseSampleButtons();
@@ -287,6 +505,9 @@
287505
handleUrlLoad();
288506
}
289507
});
508+
addMarkerButton.addEventListener('click', () => {
509+
toggleMarkerMode(!isAddingMarker);
510+
});
290511

291512
initialiseViewer();
292513
</script>

0 commit comments

Comments
 (0)