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 = '
Ricerca in corso...
';
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 = 'Nessun dataset trovato
';
return;
}
box.innerHTML = '';
data.datasets.forEach(ds => {
const item = document.createElement('div');
item.className = 'catalog-item';
item.innerHTML = `${ds.dataset_id}
${ds.title || ''}
`;
item.addEventListener('click', () => selectDataset(ds, item));
box.appendChild(item);
});
} catch (e) {
box.innerHTML = `Errore: ${e.message}
`;
} 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 = 'Caricamento variabili...';
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 = 'Nessuna variabile disponibile';
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 = `
${esc(name)}
${desc ? `${esc(desc)}` : ''}
${units ? `[${esc(units)}]` : ''}
${variableRenames[name] ? '→ ' + esc(variableRenames[name]) : ''}
`;
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} ×`;
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 =
'Cerca un dataset Copernicus per iniziare
';
document.getElementById('selectedDsBadge').style.display = 'none';
document.getElementById('variablesContainer').innerHTML =
'Seleziona un dataset per vedere le variabili';
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 = `
Dataset: ${esc(selectedDatasetId || '-')}
Variabili (${selectedVariables.size}): ${esc(vars)}
Area: ${esc(bbox)}
Periodo: ${esc(s)} → ${esc(e)}
Formato: ${esc(fmt)}
Nome: ${esc(name)}
Tags: ${esc(tags.join(', '))}
`;
}
// ── 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,'&').replace(//g,'>').replace(/"/g,'"');
}
function showToast(msg, type = 'success') {
const t = document.getElementById('toast');
t.className = `${type} show`;
t.textContent = msg;
setTimeout(() => t.classList.remove('show'), 3500);
}