Skip to content

Commit 0f111ba

Browse files
authored
Merge branch 'release-0.3' into main
2 parents 3c4f36a + 5cb2ba4 commit 0f111ba

10 files changed

Lines changed: 503 additions & 257 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
Web interface for <a href="https://github.com/kabasset/azulero">Azulero</a> by A. Basset and <a href="https://github.com/schirmermischa/eummy">Eummy</a> by M. Schirmer - two pipelines designed to produce color images from <a href="https://www.esa.int/Science_Exploration/Space_Science/Euclid">Euclid</a> space telescope data.
2020
</p>
2121

22+
<img width="1845" height="953" alt="image" src="https://github.com/user-attachments/assets/d83d007b-47cc-4c2a-b7b8-7a6ab46c00ae" />
23+
2224
## Overview
2325

2426
Azumy wraps the `azul` and `eummy` command-line pipelines into a browser-based interface. It covers the full workflow:

frontend/index.html

Lines changed: 208 additions & 182 deletions
Large diffs are not rendered by default.

frontend/js/crop.js

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,24 +57,40 @@ export async function loadCropPreview(tile) {
5757
}
5858

5959
function initDraw(canvas, ctx, img) {
60+
// Function to convert mouse event coordinates to canvas coordinates, accounting for CSS scaling
61+
const getCanvasCoords = (e) => {
62+
const rect = canvas.getBoundingClientRect();
63+
64+
// 1. compute scale factors between canvas size and displayed size
65+
const scaleX = canvas.width / rect.width;
66+
const scaleY = canvas.height / rect.height;
67+
68+
// 2. apply inverse of CSS transform to mouse coordinates
69+
return {
70+
x: (e.clientX - rect.left) * scaleX,
71+
y: (e.clientY - rect.top) * scaleY
72+
};
73+
};
74+
6075
canvas.onmousedown = e => {
61-
const r = canvas.getBoundingClientRect();
62-
startX = e.clientX - r.left;
63-
startY = e.clientY - r.top;
76+
const coords = getCanvasCoords(e);
77+
startX = coords.x;
78+
startY = coords.y;
6479
drawing = true;
6580
};
6681

6782
canvas.onmousemove = e => {
6883
if (!drawing) return;
69-
const r = canvas.getBoundingClientRect();
70-
endX = e.clientX - r.left;
71-
endY = e.clientY - r.top;
84+
const coords = getCanvasCoords(e);
85+
endX = coords.x;
86+
endY = coords.y;
87+
7288
// Redraw
7389
ctx.drawImage(img, 0, 0);
7490
ctx.strokeStyle = '#4ec9b0';
75-
ctx.lineWidth = 2;
91+
ctx.lineWidth = 2 * (canvas.width / 1000);
7692
ctx.strokeRect(startX, startY, endX - startX, endY - startY);
77-
ctx.fillStyle = 'rgba(78,201,176,0.08)';
93+
ctx.fillStyle = 'rgba(78,201,176,0.15)';
7894
ctx.fillRect(startX, startY, endX - startX, endY - startY);
7995
};
8096

@@ -88,11 +104,11 @@ function initDraw(canvas, ctx, img) {
88104
}
89105

90106
async function computeSlicing(canvas) {
91-
// Convertir les coords canvas → coords image originale
107+
// convert coords canvas → coords image
92108
const scaleX = tileWidth / canvas.width;
93109
const scaleY = tileHeight / canvas.height;
94110

95-
// L'image est flipud donc y est inversé
111+
// normalize to top-left origin and ensure x0 < x1, y0 < y1
96112
const x0 = Math.min(startX, endX) * scaleX;
97113
const x1 = Math.max(startX, endX) * scaleX;
98114
const y0 = (canvas.height - Math.max(startY, endY)) * scaleY;
@@ -119,6 +135,10 @@ function clearSelection() {
119135
export function sendCropToProcess() {
120136
const slicing = document.getElementById('cropSlicing').textContent;
121137
if (!slicing) return;
138+
const details = document.getElementById('detailsProcess');
139+
if (details) {
140+
details.open = true;
141+
}
122142
document.getElementById('processTile').value = slicing;
123-
document.getElementById('termProcess').scrollIntoView({ behavior: 'smooth' });
143+
document.getElementById('btnProcess').scrollIntoView({ behavior: 'smooth' });
124144
}

frontend/js/find.js

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,43 @@
66
import { termClear, termLine, termClassFromMessage } from './terminal.js';
77
import { progShow, progSet } from './progress.js';
88
import { openWS, API } from './websocket.js';
9-
import { initMap, goTo, loadTiling } from './map.js';
9+
import { initMap, goTo, loadTiling, drawCircle } from './map.js';
1010

1111
export let foundTiles = [];
1212
export let selectedTiles = [];
1313

1414
let selectedRa = null;
1515
let selectedDec = null;
16+
let selectedRadius = null;
1617

1718
export async function initFind() {
1819
await initMap('aladinMap');
1920
initUI();
2021
}
2122

2223
function initUI() {
23-
// Map → fill fields
24+
// Map → fill fields without radius
2425
document.addEventListener('sky:select', ({ detail: { ra, dec } }) => {
2526
selectedRa = ra;
2627
selectedDec = dec;
2728
document.getElementById('findRa').value = ra.toFixed(6);
2829
document.getElementById('findDec').value = dec.toFixed(6);
2930
});
3031

32+
// Map → fill fields with radius
33+
document.addEventListener('sky:region', ({ detail: { ra, dec, radius } }) => {
34+
selectedRa = ra;
35+
selectedDec = dec;
36+
selectedRadius = radius;
37+
document.getElementById('findRa').value = ra.toFixed(6);
38+
document.getElementById('findDec').value = dec.toFixed(6);
39+
document.getElementById('findRadius').value = radius.toFixed(6);
40+
});
41+
3142
// Manual fields → map
3243
document.getElementById('findRa')?.addEventListener('change', syncFieldsToMap);
3344
document.getElementById('findDec')?.addEventListener('change', syncFieldsToMap);
45+
document.getElementById('findRadius')?.addEventListener('change', syncFieldsToMap);
3446

3547
// Tiling input → update map overlay
3648
document.getElementById('findTiling')?.addEventListener('change', e => loadTiling(e.target.value.trim()));
@@ -78,10 +90,13 @@ function syncFieldsToMap() {
7890
if (isNaN(ra) || isNaN(dec)) return;
7991
selectedRa = ra; selectedDec = dec;
8092
goTo(ra, dec);
93+
const radius = parseFloat(document.getElementById('findRadius').value);
94+
selectedRadius = isNaN(radius) ? null : radius;
95+
drawCircle(ra, dec, selectedRadius);
8196
}
8297

8398
export function runFind() {
84-
termClear('Find');
99+
termClear('Global');
85100
document.getElementById('tilesResult').innerHTML = '';
86101
document.getElementById('sendRetrieve').style.display = 'none';
87102
foundTiles = []; selectedTiles = [];
@@ -103,9 +118,9 @@ export function runFind() {
103118
let progress = 0;
104119

105120
openWS('/find/ws', payload, {
106-
cmd: m => termLine('Find', 'c-cmd', '$ ' + m.message),
121+
cmd: m => termLine('Global', 'c-cmd', '$ ' + m.message),
107122
log: m => {
108-
termLine('Find', termClassFromMessage(m.message), m.message);
123+
termLine('Global', termClassFromMessage(m.message), m.message);
109124
const coordMatch = m.message.match(/Coordinates:\s*([\d.]+)\s*deg[^,]*,\s*([\d.]+)/);
110125
if (coordMatch) goTo(parseFloat(coordMatch[1]), parseFloat(coordMatch[2]));
111126
progress = Math.min(progress + 10, 90);
@@ -116,9 +131,9 @@ export function runFind() {
116131
addTileChip(m.data);
117132
document.getElementById('sendRetrieve').style.display = 'block';
118133
},
119-
exit: m => { if (m.code !== 0) termLine('Find', 'c-err', `exit ${m.code}`); },
134+
exit: m => { if (m.code !== 0) termLine('Global', 'c-err', `exit ${m.code}`); },
120135
done: () => { progSet('Find', 100); btn.disabled = false; },
121-
error: m => { termLine('Find', 'c-err', m.message); btn.disabled = false; },
136+
error: m => { termLine('Global', 'c-err', m.message); btn.disabled = false; },
122137
});
123138
}
124139

@@ -140,6 +155,10 @@ export function sendToRetrieve() {
140155
const toAdd = selectedTiles.length ? selectedTiles : foundTiles.map(t => t.index);
141156
const all = [...new Set([...current.split(/\s+/).filter(Boolean), ...toAdd])];
142157
document.getElementById('retrieveTiles').value = all.join(' ');
143-
document.getElementById('termRetrieve').innerHTML = '';
144-
document.getElementById('termRetrieve').scrollIntoView({ behavior: 'smooth' });
158+
const details = document.getElementById('detailsRetrieve');
159+
if (details) {
160+
details.open = true;
161+
}
162+
document.getElementById('termGlobal').innerHTML = '';
163+
document.getElementById('btnRetrieve').scrollIntoView({ behavior: 'smooth' });
145164
}

frontend/js/map.js

Lines changed: 106 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
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

98
import { 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

1518
export 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+
45105
function 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+
59149
export 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+
90178
function loadAladinScript() {
91179
return new Promise((resolve, reject) => {
92180
if (window.A) { resolve(); return; }

0 commit comments

Comments
 (0)