Files
OLD-server-architecture/copernicus/static/script.js

581 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const MARINE_API = API_URL + '/marine';
const MAPBOX_TOKEN = 'pk.eyJ1Ijoic2VzZWUzIiwiYSI6ImNtZ2dydndkMDBsNjUya3NjeW91dW41MzcifQ.M2qxj0wL1W7plRzIataojQ';
// ── State ──────────────────────────────────────────────────────────────────
let selectedDatasetId = null;
let selectedVariables = new Set();
let datasetDateRange = { min: null, max: null };
let tags = ['marine'];
let currentBbox = null;
let currentStep = 0;
let map = null;
let isDrawMode = false;
let isDrawing = false;
let drawStart = null;
let pollInterval = null;
const TOTAL_STEPS = 6;
// Variable renames: { originalName: customName }
let variableRenames = {};
let _currentRenaming = null;
// ── Init ───────────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
initMap();
renderTags();
setupTagInput();
renderDots();
showStep(0, false);
document.getElementById('catalogSearch').addEventListener('keydown', e => {
if (e.key === 'Enter') searchCatalog();
});
document.getElementById('renameInput').addEventListener('keydown', e => {
if (e.key === 'Enter') { e.preventDefault(); saveRename(); }
if (e.key === 'Escape') closeRenameModal();
});
['startDate','endDate','datasetName','outputFormat'].forEach(id => {
const el = document.getElementById(id);
if (el) el.addEventListener('change', () => { markDone(); updateSummary(); refreshNext(); });
});
});
// ── Stepper ────────────────────────────────────────────────────────────────
function showStep(i, smooth = true) {
const steps = Array.from(document.querySelectorAll('.step'));
currentStep = Math.max(0, Math.min(i, TOTAL_STEPS - 1));
steps.forEach((el, idx) => {
const active = idx === currentStep;
el.classList.toggle('active', active);
if (active) el.removeAttribute('disabled');
else el.setAttribute('disabled', '');
});
document.getElementById('prevBtn').disabled = currentStep === 0;
document.getElementById('nextBtn').textContent = currentStep === TOTAL_STEPS - 1 ? 'Fine' : 'Prossimo';
refreshNext();
updateDots();
updateSummary();
markDone();
if (smooth) {
const active = steps[currentStep];
if (active) setTimeout(() => active.scrollIntoView({ behavior: 'smooth', block: 'center' }), 60);
}
}
function refreshNext() {
document.getElementById('nextBtn').disabled = !canAdvance(currentStep);
}
function nextStep() {
if (!canAdvance(currentStep)) { showToast('Completa questo passo per continuare', 'error'); return; }
if (currentStep < TOTAL_STEPS - 1) showStep(currentStep + 1);
}
function prevStep() {
if (currentStep > 0) showStep(currentStep - 1);
}
function canAdvance(i) {
switch (i) {
case 0: return !!selectedDatasetId;
case 1: return selectedVariables.size > 0;
case 2: return !!currentBbox;
case 3: {
const s = document.getElementById('startDate').value;
const e = document.getElementById('endDate').value;
return !!(s && e && s <= e);
}
case 4: return !!document.getElementById('datasetName').value.trim();
default: return true;
}
}
// ── Dots ───────────────────────────────────────────────────────────────────
function renderDots() {
const c = document.getElementById('progressDots');
c.innerHTML = '';
for (let i = 0; i < TOTAL_STEPS; i++) {
const d = document.createElement('button');
d.className = 'progress-dot';
d.setAttribute('aria-label', `Passo ${i + 1}`);
d.addEventListener('click', () => { if (i <= currentStep) showStep(i); });
c.appendChild(d);
}
updateDots();
}
function updateDots() {
Array.from(document.getElementById('progressDots').children)
.forEach((d, i) => d.classList.toggle('active', i === currentStep));
}
// ── Done badges ────────────────────────────────────────────────────────────
function markDone() {
const steps = document.querySelectorAll('.step');
const checks = [
!!selectedDatasetId,
selectedVariables.size > 0,
!!currentBbox,
canAdvance(3),
!!document.getElementById('datasetName').value.trim(),
false,
];
steps.forEach((el, i) => el.classList.toggle('done', checks[i] === true));
}
// ── Catalog ────────────────────────────────────────────────────────────────
async function searchCatalog() {
const q = document.getElementById('catalogSearch').value.trim();
const btn = document.getElementById('searchBtn');
const box = document.getElementById('catalogResults');
box.innerHTML = '<div class="catalog-empty"><span class="spin"></span>Ricerca in corso...</div>';
btn.disabled = true;
try {
const params = q ? `?search=${encodeURIComponent(q)}&limit=30` : '?limit=30';
const res = await MEB_AUTH.fetch(`${MARINE_API}/catalog${params}`);
const data = await res.json();
if (!res.ok) throw new Error(data.detail || 'Errore catalogo');
if (!data.datasets?.length) {
box.innerHTML = '<div class="catalog-empty">Nessun dataset trovato</div>';
return;
}
box.innerHTML = '';
data.datasets.forEach(ds => {
const item = document.createElement('div');
item.className = 'catalog-item';
item.innerHTML = `<div class="ds-id">${ds.dataset_id}</div><div class="ds-title">${ds.title || ''}</div>`;
item.addEventListener('click', () => selectDataset(ds, item));
box.appendChild(item);
});
} catch (e) {
box.innerHTML = `<div class="catalog-empty" style="color:var(--danger)">Errore: ${e.message}</div>`;
} finally {
btn.disabled = false;
}
}
async function selectDataset(ds, itemEl) {
document.querySelectorAll('.catalog-item').forEach(i => i.classList.remove('selected'));
itemEl.classList.add('selected');
selectedDatasetId = ds.dataset_id;
const badge = document.getElementById('selectedDsBadge');
badge.textContent = ds.dataset_id;
badge.style.display = 'block';
// Reset dependent steps
selectedVariables.clear();
const vBox = document.getElementById('variablesContainer');
vBox.innerHTML = '<span class="spin"></span><span style="color:var(--text-secondary);font-size:0.85rem;">Caricamento variabili...</span>';
try {
const res = await MEB_AUTH.fetch(`${MARINE_API}/catalog/${encodeURIComponent(ds.dataset_id)}`);
const info = await res.json();
renderVariables(info.variables || ds.variables || []);
if (info.min_longitude != null) {
setBboxAndFit(info.min_longitude, info.max_longitude, info.min_latitude, info.max_latitude);
}
if (info.start_datetime) {
prefillDates(info.start_datetime, info.end_datetime);
}
} catch {
renderVariables(ds.variables || []);
}
markDone();
refreshNext();
// Auto-advance to variables step
setTimeout(() => showStep(1), 700);
}
// ── Variables ──────────────────────────────────────────────────────────────
function renderVariables(vars) {
const c = document.getElementById('variablesContainer');
if (!vars?.length) {
c.innerHTML = '<span style="color:var(--text-secondary);font-size:0.85rem;">Nessuna variabile disponibile</span>';
updateVarCount();
return;
}
c.innerHTML = '';
vars.forEach(v => {
const name = typeof v === 'string' ? v : v.short_name;
const desc = typeof v === 'object' ? (v.description || v.standard_name || '') : '';
const units = typeof v === 'object' && v.units ? v.units : '';
const chip = document.createElement('div');
chip.className = 'var-chip';
chip.dataset.name = name;
chip.innerHTML = `
<span class="var-name">${esc(name)}</span>
${desc ? `<span class="var-desc" title="${esc(desc)}">${esc(desc)}</span>` : ''}
${units ? `<span class="var-units">[${esc(units)}]</span>` : ''}
<button class="rename-btn" onclick="event.stopPropagation(); openRenameModal(this.closest('.var-chip').dataset.name)">✏ Rinomina</button>
<span class="rename-badge">${variableRenames[name] ? '→ ' + esc(variableRenames[name]) : ''}</span>
`;
chip.addEventListener('click', () => toggleVar(chip, name));
c.appendChild(chip);
});
updateVarCount();
}
function toggleVar(chip, name) {
if (selectedVariables.has(name)) { selectedVariables.delete(name); chip.classList.remove('selected'); }
else { selectedVariables.add(name); chip.classList.add('selected'); }
updateVarCount(); markDone(); refreshNext();
}
function updateVarCount() {
const n = selectedVariables.size;
document.getElementById('varCount').textContent =
n === 0 ? 'Nessuna selezionata' : `${n} selezionat${n === 1 ? 'a' : 'e'}`;
}
function selectAllVars() {
document.querySelectorAll('.var-chip').forEach(chip => {
chip.classList.add('selected');
selectedVariables.add(chip.dataset.name);
});
updateVarCount(); markDone(); refreshNext();
}
function deselectAllVars() {
document.querySelectorAll('.var-chip').forEach(chip => chip.classList.remove('selected'));
selectedVariables.clear();
updateVarCount(); markDone(); refreshNext();
}
// ── Dates ──────────────────────────────────────────────────────────────────
function prefillDates(minDate, maxDate) {
datasetDateRange = { min: minDate, max: maxDate };
const s = document.getElementById('startDate');
const e = document.getElementById('endDate');
if (minDate) { s.min = minDate; s.value = minDate; e.min = minDate; }
if (maxDate) { e.max = maxDate; e.value = maxDate; s.max = maxDate; }
const hint = document.getElementById('dateRangeHint');
if (minDate && maxDate) hint.textContent = `Dati disponibili: ${minDate}${maxDate}`;
markDone(); updateSummary();
}
// ── Map ────────────────────────────────────────────────────────────────────
function initMap() {
mapboxgl.accessToken = MAPBOX_TOKEN;
map = new mapboxgl.Map({
container: 'mapContainer',
style: 'mapbox://styles/mapbox/dark-v11',
center: [14, 42], zoom: 3.5,
attributionControl: false,
});
map.addControl(new mapboxgl.NavigationControl({ showCompass: false }), 'top-right');
map.addControl(new mapboxgl.AttributionControl({ compact: true }), 'bottom-right');
map.on('load', () => {
map.addSource('bbox-rect', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });
map.addLayer({ id: 'bbox-fill', type: 'fill', source: 'bbox-rect', paint: { 'fill-color': '#00a7f5', 'fill-opacity': 0.13 } });
map.addLayer({ id: 'bbox-line', type: 'line', source: 'bbox-rect', paint: { 'line-color': '#00a7f5', 'line-width': 2, 'line-dasharray': [4, 2] } });
});
map.getCanvas().addEventListener('mousedown', onCanvasMouseDown, true);
window.addEventListener('mousemove', onWindowMouseMove);
window.addEventListener('mouseup', onWindowMouseUp);
}
function startDraw() {
if (!map) return;
isDrawMode = true;
map.dragPan.disable(); map.boxZoom.disable();
document.getElementById('mapContainer').classList.add('draw-mode');
const btn = document.getElementById('drawBtn');
btn.textContent = 'Clicca e trascina…';
btn.classList.replace('secondary', 'primary');
}
function exitDrawMode() {
isDrawMode = isDrawing = false; drawStart = null;
map.dragPan.enable(); map.boxZoom.enable();
document.getElementById('mapContainer').classList.remove('draw-mode', 'drawing');
const btn = document.getElementById('drawBtn');
btn.textContent = 'Disegna area';
btn.classList.replace('primary', 'secondary');
}
function onCanvasMouseDown(e) {
if (!isDrawMode) return;
e.preventDefault(); e.stopPropagation();
isDrawing = true;
drawStart = map.unproject([e.offsetX, e.offsetY]);
document.getElementById('mapContainer').classList.add('drawing');
}
function onWindowMouseMove(e) {
if (!isDrawing || !drawStart) return;
const rc = map.getCanvas().getBoundingClientRect();
_drawRect(drawStart, map.unproject([e.clientX - rc.left, e.clientY - rc.top]));
}
function onWindowMouseUp(e) {
if (!isDrawing || !drawStart) return;
const rc = map.getCanvas().getBoundingClientRect();
const end = map.unproject([e.clientX - rc.left, e.clientY - rc.top]);
setBbox(
Math.min(drawStart.lng, end.lng), Math.max(drawStart.lng, end.lng),
Math.min(drawStart.lat, end.lat), Math.max(drawStart.lat, end.lat)
);
exitDrawMode();
}
function _drawRect(a, b) {
if (!map.getSource('bbox-rect')) return;
map.getSource('bbox-rect').setData({
type: 'Feature',
geometry: { type: 'Polygon', coordinates: [[[a.lng,a.lat],[b.lng,a.lat],[b.lng,b.lat],[a.lng,b.lat],[a.lng,a.lat]]] },
});
}
function setBbox(minLon, maxLon, minLat, maxLat) {
currentBbox = { minLon, maxLon, minLat, maxLat };
document.getElementById('minLon').value = minLon.toFixed(4);
document.getElementById('maxLon').value = maxLon.toFixed(4);
document.getElementById('minLat').value = minLat.toFixed(4);
document.getElementById('maxLat').value = maxLat.toFixed(4);
document.getElementById('bboxReadout').textContent =
`${minLon.toFixed(2)}°/${minLat.toFixed(2)}° → ${maxLon.toFixed(2)}°/${maxLat.toFixed(2)}°`;
_drawRect({ lng: minLon, lat: minLat }, { lng: maxLon, lat: maxLat });
markDone(); refreshNext();
}
function setBboxAndFit(minLon, maxLon, minLat, maxLat) {
const doIt = () => {
setBbox(minLon, maxLon, minLat, maxLat);
map.fitBounds([[minLon, minLat], [maxLon, maxLat]], { padding: 40, maxZoom: 10, duration: 600 });
};
if (!map) return;
if (map.isStyleLoaded()) doIt(); else map.once('load', doIt);
}
function clearBbox() {
currentBbox = null;
['minLon','maxLon','minLat','maxLat'].forEach(id => document.getElementById(id).value = '');
document.getElementById('bboxReadout').textContent = '';
if (map?.getSource('bbox-rect')) map.getSource('bbox-rect').setData({ type: 'FeatureCollection', features: [] });
markDone(); refreshNext();
}
// ── Tags ───────────────────────────────────────────────────────────────────
function setupTagInput() {
const inp = document.getElementById('tagInput');
inp.addEventListener('keydown', e => {
if ((e.key === 'Enter' || e.key === ',') && inp.value.trim()) {
e.preventDefault();
const t = inp.value.trim().replace(/,/g,'').toLowerCase();
if (t && !tags.includes(t)) { tags.push(t); renderTags(); }
inp.value = '';
} else if (e.key === 'Backspace' && !inp.value && tags.length) {
tags.pop(); renderTags();
}
});
}
function renderTags() {
const wrap = document.getElementById('tagsWrap');
const inp = document.getElementById('tagInput');
wrap.innerHTML = '';
tags.forEach(t => {
const chip = document.createElement('span');
chip.className = 'tag-chip';
chip.innerHTML = `${t} <span class="rm" onclick="removeTag('${t}')">×</span>`;
wrap.appendChild(chip);
});
wrap.appendChild(inp);
}
function removeTag(t) { tags = tags.filter(x => x !== t); renderTags(); }
// ── Download ───────────────────────────────────────────────────────────────
async function startDownload() {
if (!selectedDatasetId) return showToast('Seleziona un dataset', 'error');
if (!selectedVariables.size) return showToast('Seleziona almeno una variabile', 'error');
if (!currentBbox) return showToast("Disegna un'area sulla mappa", 'error');
if (!document.getElementById('startDate').value ||
!document.getElementById('endDate').value) return showToast('Inserisci le date', 'error');
if (!document.getElementById('datasetName').value.trim()) return showToast('Inserisci un nome', 'error');
const body = {
dataset_id: selectedDatasetId,
variables: Array.from(selectedVariables),
min_longitude: parseFloat(document.getElementById('minLon').value),
max_longitude: parseFloat(document.getElementById('maxLon').value),
min_latitude: parseFloat(document.getElementById('minLat').value),
max_latitude: parseFloat(document.getElementById('maxLat').value),
start_date: document.getElementById('startDate').value,
end_date: document.getElementById('endDate').value,
format: document.getElementById('outputFormat').value,
nome: document.getElementById('datasetName').value.trim(),
tags: [...tags],
notes: document.getElementById('datasetNotes').value.trim(),
variable_renames: { ...variableRenames },
};
const btn = document.getElementById('downloadBtn');
const prog = document.getElementById('downloadProgress');
btn.disabled = true;
prog.style.display = 'block';
setProgress(0, 'Avvio download...');
try {
const res = await MEB_AUTH.fetch(`${MARINE_API}/jobs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || 'Errore avvio job');
pollJob(data.job_id, btn, prog);
} catch (e) {
showToast(`Errore: ${e.message}`, 'error');
btn.disabled = false;
prog.style.display = 'none';
}
}
function pollJob(jobId, btn, prog) {
clearInterval(pollInterval);
pollInterval = setInterval(async () => {
try {
const res = await MEB_AUTH.fetch(`${MARINE_API}/jobs/${jobId}`);
const job = await res.json();
setProgress(job.progress, job.message);
if (job.status === 'done') {
clearInterval(pollInterval);
btn.disabled = false;
showToast('Dataset salvato con successo!', 'success');
setTimeout(resetAll, 1800);
} else if (job.status === 'error') {
clearInterval(pollInterval);
btn.disabled = false;
showToast(`Errore: ${job.message}`, 'error');
prog.style.display = 'none';
}
} catch { /* transient */ }
}, 2000);
}
function setProgress(pct, msg) {
document.getElementById('progressFill').style.width = pct + '%';
document.getElementById('progressMsg').textContent = msg;
}
// ── Reset ──────────────────────────────────────────────────────────────────
function resetAll() {
selectedDatasetId = null;
selectedVariables.clear();
datasetDateRange = { min: null, max: null };
variableRenames = {};
tags = ['marine'];
['startDate','endDate','datasetName','datasetNotes'].forEach(id => {
const el = document.getElementById(id);
if (el) { el.value = ''; el.min = ''; el.max = ''; }
});
document.getElementById('catalogResults').innerHTML =
'<div class="catalog-empty">Cerca un dataset Copernicus per iniziare</div>';
document.getElementById('selectedDsBadge').style.display = 'none';
document.getElementById('variablesContainer').innerHTML =
'<span style="color:var(--text-secondary);font-size:0.85rem;">Seleziona un dataset per vedere le variabili</span>';
document.getElementById('dateRangeHint').textContent = '';
document.getElementById('downloadProgress').style.display = 'none';
clearBbox();
renderTags();
showStep(0);
}
// ── Summary ────────────────────────────────────────────────────────────────
function updateSummary() {
if (currentStep !== 5) return;
const s = document.getElementById('startDate').value || '-';
const e = document.getElementById('endDate').value || '-';
const name = document.getElementById('datasetName').value || '-';
const fmt = document.getElementById('outputFormat').value || '-';
const vars = Array.from(selectedVariables).join(', ') || '-';
const bbox = currentBbox
? `${currentBbox.minLon.toFixed(2)},${currentBbox.minLat.toFixed(2)}${currentBbox.maxLon.toFixed(2)},${currentBbox.maxLat.toFixed(2)}`
: '-';
document.getElementById('summaryContent').innerHTML = `
<div><strong>Dataset:</strong> ${esc(selectedDatasetId || '-')}</div>
<div><strong>Variabili (${selectedVariables.size}):</strong> ${esc(vars)}</div>
<div><strong>Area:</strong> ${esc(bbox)}</div>
<div><strong>Periodo:</strong> ${esc(s)}${esc(e)}</div>
<div><strong>Formato:</strong> ${esc(fmt)}</div>
<div><strong>Nome:</strong> ${esc(name)}</div>
<div><strong>Tags:</strong> ${esc(tags.join(', '))}</div>
`;
}
// ── Rename modal ───────────────────────────────────────────────────────────
function openRenameModal(varName) {
_currentRenaming = varName;
document.getElementById('renameVarLabel').textContent = varName;
document.getElementById('renameInput').value = variableRenames[varName] || '';
document.getElementById('renameDeleteBtn').style.display = variableRenames[varName] ? 'inline-flex' : 'none';
document.getElementById('renameModal').classList.add('visible');
setTimeout(() => document.getElementById('renameInput').select(), 50);
}
function saveRename() {
if (!_currentRenaming) return;
const val = document.getElementById('renameInput').value.trim();
if (!val) { deleteRename(); return; }
variableRenames[_currentRenaming] = val;
_updateRenameBadge(_currentRenaming, val);
closeRenameModal();
}
function deleteRename() {
if (!_currentRenaming) return;
delete variableRenames[_currentRenaming];
_updateRenameBadge(_currentRenaming, '');
closeRenameModal();
}
function closeRenameModal() {
document.getElementById('renameModal').classList.remove('visible');
_currentRenaming = null;
}
function _updateRenameBadge(varName, text) {
const chip = [...document.querySelectorAll('.var-chip')].find(c => c.dataset.name === varName);
if (!chip) return;
chip.querySelector('.rename-badge').textContent = text ? '→ ' + text : '';
}
// ── Helpers ────────────────────────────────────────────────────────────────
function esc(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function showToast(msg, type = 'success') {
const t = document.getElementById('toast');
t.className = `${type} show`;
t.textContent = msg;
setTimeout(() => t.classList.remove('show'), 3500);
}