Skip to content

Commit acaff37

Browse files
committed
Add landing
1 parent c9fac3e commit acaff37

5 files changed

Lines changed: 479 additions & 113 deletions

File tree

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# NASA Sky Explorer Prototype
1+
# NASA FITS Explorer
22

3-
A barebones FastAPI project that serves the landing page for the NASA Sky Explorer Prototype
3+
A FastAPI-powered prototype that serves an interactive Aladin Lite interface for visualising NASA (and allied) FITS imagery. Users can upload local FITS files, paste remote URLs, or launch curated samples spanning multiple missions.
44

55
## Quickstart
66

@@ -18,7 +18,15 @@ A barebones FastAPI project that serves the landing page for the NASA Sky Explor
1818
uvicorn src.server:app --reload
1919
```
2020

21-
3. Open <http://127.0.0.1:8000/> in your browser to view the page titled **“Minimal FastAPI App.”**
21+
3. Open <http://127.0.0.1:8000/> in your browser and click **“Launch the viewer.”** The FITS explorer lives at <http://127.0.0.1:8000/aladin>.
22+
23+
### Working with FITS datasets
24+
25+
- **Local files:** Use the “Open local FITS file” control. Files never leave the browser; they stream directly into Aladin Lite.
26+
- **Remote URLs:** Paste a direct link to a FITS file (for example, from NASA’s FITS sample archive or a mission mirror). The viewer requests the data client-side, so the remote server must allow cross-origin (`CORS`) downloads.
27+
- **Curated samples:** Try the Hubble, GALEX, or WISE presets to demonstrate different wavelengths and resolutions. Each sample automatically recentres and applies a mission-appropriate colour map.
28+
29+
The HUD panel displays the currently loaded source and field of view. Loading very large FITS mosaics may take several seconds depending on bandwidth.
2230

2331
## Linting
2432

@@ -48,7 +56,7 @@ NASASpaceAppsChallenge2025/
4856
└── deploy.yml # Continuous deployment pipeline for EC2
4957
```
5058

51-
Feel free to build on this foundation for richer APIs or interfaces.
59+
Feel free to extend the UI with annotations, multi-layer comparisons, or temporal sliders to address the broader Space Apps challenge goals.
5260

5361
## Continuous deployment to EC2
5462

src/server.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@
1010
WEB_DIR = BASE_DIR / "web"
1111
INDEX_PATH = WEB_DIR / "index.html"
1212
ALADIN_PATH = WEB_DIR / "aladin.html"
13-
FAVICON_SVG = """<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'>\n<rect width='32' height='32' rx='8' fill='#0f172a'/>\n<path fill='#facc15' d='M16 4l3.4 7 7.6.7-5.8 5.2 1.8 7.6-7-4.2-7 4.2 1.8-7.6-5.8-5.2 7.6-.7z'/>\n</svg>"""
13+
FAVICON_SVG = (
14+
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'>\n"
15+
"<rect width='32' height='32' rx='8' fill='#0f172a'/>\n"
16+
"<path fill='#facc15' d='M16 4l3.4 7 7.6.7-5.8 5.2 1.8 7.6-7-4.2-7 4.2 "
17+
"1.8-7.6-5.8-5.2 7.6-.7z'/>\n"
18+
"</svg>"
19+
)
1420

1521
app = FastAPI(title="NASA Sky Explorer Prototype")
1622
app.mount("/static", StaticFiles(directory=WEB_DIR), name="static")
@@ -43,6 +49,7 @@ def favicon() -> Response:
4349

4450
return Response(content=FAVICON_SVG, media_type="image/svg+xml")
4551

52+
4653
if __name__ == "__main__":
4754
import uvicorn
4855

web/aladin.html

Lines changed: 228 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -3,103 +3,267 @@
33
<head>
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width" />
6-
<title>NASA Sky Viewer</title>
6+
<title>NASA FITS Explorer</title>
77
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%230f172a'/%3E%3Cpath fill='%23facc15' d='M16 4l3.4 7 7.6.7-5.8 5.2 1.8 7.6-7-4.2-7 4.2 1.8-7.6-5.8-5.2 7.6-.7z'/%3E%3C/svg%3E" />
88
<link rel="stylesheet" href="/static/styles.css" />
99
<link rel="stylesheet" href="https://aladin.cds.unistra.fr/AladinLite/api/v3/latest/aladin.min.css" />
1010
</head>
1111
<body>
12-
<div id="aladin-lite-div"></div>
12+
<div id="aladin-lite-div" aria-label="Interactive FITS viewer"></div>
1313
<div id="aladin-overlay" class="overlay" role="status" aria-live="polite">
14-
Loading the NASA sky map…
14+
<span id="overlay-message">Loading the NASA sky map…</span>
1515
</div>
16-
<div class="hud" aria-live="polite">
17-
<div class="status" id="status-message">
18-
Target: NGC 2175 · Survey: WISE (Infrared)
19-
</div>
16+
<aside class="hud" aria-live="polite">
17+
<p class="title">NASA FITS Explorer</p>
18+
<p class="status" id="status-message">Select a FITS source to start exploring.</p>
2019
<div class="controls">
21-
<button type="button" id="reset-view">Reset view</button>
22-
<select id="survey-select" aria-label="Select survey layer">
23-
<option value="P/allWISE/color" selected>WISE (Infrared)</option>
24-
<option value="P/DSS2/color">DSS2 (Optical)</option>
25-
<option value="P/2MASS/color">2MASS (Near Infrared)</option>
26-
<option value="P/GALEXGR6/AIS/color">GALEX (Ultraviolet)</option>
27-
</select>
20+
<label class="file-picker">
21+
<input id="fits-file" type="file" accept=".fits,.fit,application/fits" />
22+
<span>Open local FITS file</span>
23+
</label>
24+
<div class="url-loader">
25+
<input id="fits-url" type="url" placeholder="https://data.nasa.gov/sample.fits" aria-label="FITS file URL" />
26+
<button type="button" id="load-url">Load URL</button>
27+
</div>
2828
</div>
29-
<div class="notice hidden" id="aladin-error" role="alert"></div>
30-
</div>
29+
<div class="samples">
30+
<span class="samples-label">Try a sample dataset:</span>
31+
<div class="sample-buttons">
32+
<button
33+
type="button"
34+
data-fits-url="https://cdsarc.cds.unistra.fr/saadavizier/download?oid=864972989978905533"
35+
data-label="WISE Infrared — IC 434 & Horsehead Nebula"
36+
data-colormap="magma"
37+
>
38+
Horsehead Nebula (WISE)
39+
</button>
40+
<button
41+
type="button"
42+
data-fits-url="https://fits.gsfc.nasa.gov/samples/NGC6543.fits"
43+
data-label="HST WFPC2 — NGC 6543 (Cat's Eye)"
44+
data-colormap="viridis"
45+
>
46+
Cat's Eye Nebula (HST)
47+
</button>
48+
<button
49+
type="button"
50+
data-fits-url="https://fits.gsfc.nasa.gov/samples/FUV.fits"
51+
data-label="GALEX FUV — M101 Spiral"
52+
data-colormap="plasma"
53+
>
54+
Pinwheel Galaxy (GALEX)
55+
</button>
56+
</div>
57+
</div>
58+
<p class="notice" id="aladin-hint">
59+
Upload high-resolution FITS files from NASA missions or paste a link to remote data sets. Large imagery may take a moment to render.
60+
</p>
61+
<p class="notice hidden" id="aladin-error" role="alert"></p>
62+
</aside>
3163
<script type="module">
3264
import { A } from 'https://aladin.cds.unistra.fr/AladinLite/api/v3/latest/aladin.min.js';
3365

3466
const overlay = document.getElementById('aladin-overlay');
67+
const overlayMessage = document.getElementById('overlay-message');
3568
const statusMessage = document.getElementById('status-message');
3669
const errorBox = document.getElementById('aladin-error');
37-
const surveySelect = document.getElementById('survey-select');
38-
const resetButton = document.getElementById('reset-view');
39-
40-
const defaultConfig = {
41-
survey: 'P/allWISE/color',
42-
projection: 'AIT',
43-
fov: 1.5,
44-
target: 'NGC 2175',
45-
cooFrame: 'galactic',
46-
showCooGrid: true,
47-
fullScreen: true
48-
};
70+
const fileInput = document.getElementById('fits-file');
71+
const urlInput = document.getElementById('fits-url');
72+
const loadUrlButton = document.getElementById('load-url');
73+
const sampleButtons = Array.from(document.querySelectorAll('[data-fits-url]'));
4974

5075
let aladinInstance = null;
76+
let currentRequestId = 0;
5177

52-
function updateStatus(target, surveyLabel) {
53-
statusMessage.textContent = `Target: ${target} · Survey: ${surveyLabel}`;
54-
}
78+
const DEFAULT_SAMPLE = sampleButtons.length
79+
? {
80+
url: sampleButtons[0].dataset.fitsUrl,
81+
label: sampleButtons[0].dataset.label,
82+
colormap: sampleButtons[0].dataset.colormap
83+
}
84+
: null;
5585

56-
function handleError(error) {
57-
console.error('Failed to initialise Aladin Lite', error);
86+
function setOverlay(message) {
87+
overlayMessage.textContent = message;
5888
overlay.classList.remove('hidden');
59-
overlay.textContent = 'Unable to load the NASA sky map right now.';
60-
errorBox.classList.remove('hidden');
61-
errorBox.textContent = error instanceof Error ? error.message : String(error);
6289
}
6390

64-
try {
65-
await A.init;
66-
aladinInstance = await A.aladin('#aladin-lite-div', defaultConfig);
91+
function clearOverlay() {
6792
overlay.classList.add('hidden');
93+
overlayMessage.textContent = 'Loading the NASA sky map…';
94+
}
95+
96+
function showError(message) {
97+
errorBox.textContent = message;
98+
errorBox.classList.remove('hidden');
99+
}
100+
101+
function clearError() {
102+
errorBox.textContent = '';
68103
errorBox.classList.add('hidden');
69-
updateStatus(defaultConfig.target, 'WISE (Infrared)');
104+
}
70105

71-
resetButton.addEventListener('click', () => {
72-
if (!aladinInstance) {
73-
return;
74-
}
106+
function updateStatus(label, fov) {
107+
const parts = [];
108+
if (label) {
109+
parts.push(`Source: ${label}`);
110+
}
111+
if (Number.isFinite(fov)) {
112+
const span = Math.max(0.01, fov * 2);
113+
parts.push(`FoV ≈ ${span.toFixed(2)}°`);
114+
}
115+
statusMessage.textContent = parts.join(' · ') || 'Ready to load a FITS image.';
116+
}
117+
118+
function focusOnImage(ra, dec, fov) {
119+
if (Number.isFinite(ra) && Number.isFinite(dec)) {
120+
aladinInstance.gotoRaDec(ra, dec);
121+
}
122+
if (Number.isFinite(fov)) {
123+
const clamped = Math.min(60, Math.max(0.01, fov * 2));
124+
aladinInstance.setFoV(clamped);
125+
}
126+
}
127+
128+
function configureImage(image, colormap) {
129+
if (!image) {
130+
return;
131+
}
132+
try {
133+
image.setColormap(colormap || 'magma', { stretch: 'sqrt' });
134+
} catch (error) {
135+
console.warn('Unable to update the FITS colormap', error);
136+
}
137+
}
138+
139+
function deriveLabel(source) {
140+
if (typeof source === 'string') {
75141
try {
76-
aladinInstance.gotoObject(defaultConfig.target);
77-
aladinInstance.setFov(defaultConfig.fov);
78-
aladinInstance.setImageSurvey(defaultConfig.survey);
79-
surveySelect.value = defaultConfig.survey;
80-
updateStatus(defaultConfig.target, 'WISE (Infrared)');
81-
} catch (error) {
82-
handleError(error);
142+
const url = new URL(source);
143+
return url.hostname;
144+
} catch {
145+
return source;
83146
}
84-
});
147+
}
148+
if (source && typeof source.name === 'string') {
149+
return `Local file: ${source.name}`;
150+
}
151+
return 'Custom FITS';
152+
}
85153

86-
surveySelect.addEventListener('change', (event) => {
87-
if (!aladinInstance) {
154+
function loadFits(source, options = {}) {
155+
if (!aladinInstance || !source) {
156+
return;
157+
}
158+
159+
const requestId = ++currentRequestId;
160+
const label = options.label || deriveLabel(source);
161+
162+
clearError();
163+
setOverlay(`Loading ${label}…`);
164+
165+
const handleSuccess = (ra, dec, fov, image) => {
166+
if (requestId !== currentRequestId) {
88167
return;
89168
}
90-
try {
91-
const select = event.target;
92-
const surveyId = select.value;
93-
const label = select.options[select.selectedIndex].text;
94-
aladinInstance.setImageSurvey(surveyId);
95-
updateStatus(defaultConfig.target, label);
96-
} catch (error) {
97-
handleError(error);
169+
configureImage(image, options.colormap);
170+
focusOnImage(ra, dec, fov);
171+
updateStatus(label, fov);
172+
clearOverlay();
173+
};
174+
175+
const handleError = (error) => {
176+
if (requestId !== currentRequestId) {
177+
return;
98178
}
179+
console.error('Failed to load FITS data', error);
180+
clearOverlay();
181+
showError('Unable to load the FITS data. Please verify the file or URL and try again.');
182+
};
183+
184+
try {
185+
const result = aladinInstance.displayFITS(source, options.params || {}, handleSuccess, handleError);
186+
if (result && typeof result.then === 'function') {
187+
result.catch(handleError);
188+
}
189+
} catch (error) {
190+
handleError(error);
191+
}
192+
}
193+
194+
function handleFileSelection(event) {
195+
const [file] = event.target.files || [];
196+
if (!file) {
197+
return;
198+
}
199+
loadFits(file, { label: `Local file: ${file.name}` });
200+
event.target.value = '';
201+
}
202+
203+
function handleUrlLoad() {
204+
const url = urlInput.value.trim();
205+
if (!url) {
206+
showError('Please enter a FITS file URL.');
207+
return;
208+
}
209+
try {
210+
new URL(url);
211+
} catch {
212+
showError('The URL appears to be invalid. Please double-check and try again.');
213+
return;
214+
}
215+
loadFits(url, { label: url });
216+
}
217+
218+
function initialiseSampleButtons() {
219+
sampleButtons.forEach((button) => {
220+
button.addEventListener('click', () => {
221+
const { fitsUrl, label, colormap } = button.dataset;
222+
loadFits(fitsUrl, { label, colormap });
223+
});
99224
});
100-
} catch (error) {
101-
handleError(error);
102225
}
226+
227+
async function initialiseViewer() {
228+
try {
229+
await A.init;
230+
aladinInstance = await A.aladin('#aladin-lite-div', {
231+
cooFrame: 'icrs',
232+
projection: 'AIT',
233+
showCooGrid: false,
234+
fullScreen: true,
235+
fov: 5,
236+
target: 'M 31'
237+
});
238+
239+
initialiseSampleButtons();
240+
if (DEFAULT_SAMPLE) {
241+
loadFits(DEFAULT_SAMPLE.url, DEFAULT_SAMPLE);
242+
} else {
243+
clearOverlay();
244+
updateStatus(null);
245+
}
246+
} catch (error) {
247+
console.error('Aladin Lite failed to initialise', error);
248+
showError('The Aladin Lite viewer could not be initialised. Please refresh the page.');
249+
clearOverlay();
250+
}
251+
}
252+
253+
fileInput.addEventListener('change', handleFileSelection);
254+
fileInput.addEventListener('click', () => {
255+
clearError();
256+
});
257+
258+
loadUrlButton.addEventListener('click', handleUrlLoad);
259+
urlInput.addEventListener('keydown', (event) => {
260+
if (event.key === 'Enter') {
261+
event.preventDefault();
262+
handleUrlLoad();
263+
}
264+
});
265+
266+
initialiseViewer();
103267
</script>
104268
</body>
105269
</html>

0 commit comments

Comments
 (0)