This commit is contained in:
Felix Delattre 2026-04-11 19:25:42 +02:00
parent ec3aac3ec3
commit db14a71228
7 changed files with 374 additions and 108 deletions

View file

@ -5,6 +5,7 @@
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/geotiff@2.0.7/dist-browser/geotiff.js"></script>
<script src="common.js"></script>
<script src="https://cdn.jsdelivr.net/npm/proj4@2.9.0/dist/proj4.js"></script>
<style>
body { margin: 0; font-family: sans-serif; }
@ -42,6 +43,7 @@
<a href="prepared.html">Prepared</a>
<a href="fusion.html" class="active">Fusion</a>
<a href="postprocessed.html">Postprocessed</a>
<a href="metrics.html">Metrics</a>
</div>
<h1 id="siteName">Innsbruck</h1>
<div class="season-row"><h2 id="season">2024</h2><span class="download-links" id="downloadLinks"></span></div>
@ -60,11 +62,16 @@
<option value="20">σ=20</option>
<option value="30">σ=30</option>
</select>
<label>Mode:</label>
<select id="fusionModeSelect" title="BtI = reflectance fusion; ItB = GCC fusion">
<option value="bti">BtI (REFL)</option>
<option value="itb">ItB (GCC)</option>
</select>
</div>
<input type="range" id="dateSlider" min="0" max="365" value="0">
<div id="dateDisplay">2024-01-01</div>
</div>
<div class="map-label">Fusion RGB (closest available)</div>
<div class="map-label" id="mapLabelFusion">Fusion RGB (closest available)</div>
<div id="mapDate" class="map-date"></div>
<div id="fusionMap"></div>
<div id="plots">
@ -81,7 +88,7 @@
proj4.defs("EPSG:4326", "+proj=longlat +datum=WGS84 +no_defs");
let siteName = "innsbruck", season = "2024";
let strategy = "aggressive", sigma = "20";
let strategy = "aggressive", sigma = "20", fusionMode = "bti";
let sitePosition = [47.116171, 11.320308];
let start = new Date(2024, 0, 1);
let availableSiteSeasons = {};
@ -98,10 +105,13 @@
return Math.floor((new Date(y, m - 1, d) - start) / 86400000);
};
function getPreparedBase() {
return fusionMode === "itb" ? `prepared_${strategy}_itb` : `prepared_${strategy}`;
}
function getFusionDir() {
return sigma === "30"
? `data/${siteName}/${season}/prepared_${strategy}/fusion_sigma30`
: `data/${siteName}/${season}/prepared_${strategy}/fusion`;
const sub = sigma === "30" ? "fusion_sigma30" : "fusion";
return `data/${siteName}/${season}/${getPreparedBase()}/${sub}`;
}
function getFusionTimeseriesDir() {
@ -110,15 +120,28 @@
async function loadTimeseries() {
const sub = getFusionTimeseriesDir();
const base = `data/${siteName}/${season}/${getPreparedBase()}`;
try {
const base = `data/${siteName}/${season}/prepared_${strategy}`;
const [n, g, b] = await Promise.all([
fetch(`${base}/ndvi/${sub}/timeseries.json`).then(r => r.ok ? r.json() : []),
fetch(`${base}/gcc/${sub}/timeseries.json`).then(r => r.ok ? r.json() : []),
fetch(`${base}/bands/${sub}/timeseries.json`).then(r => r.ok ? r.json() : [])
]);
ndviTs = n; gccTs = g; bandsTs = b;
} catch { ndviTs = []; gccTs = []; bandsTs = []; }
if (fusionMode === "itb") {
const g = await fetch(`${base}/gcc/${sub}/timeseries.json`).then((r) => (r.ok ? r.json() : []));
ndviTs = [];
gccTs = g;
bandsTs = [];
} else {
const [n, g, b] = await Promise.all([
fetch(`${base}/ndvi/${sub}/timeseries.json`).then((r) => (r.ok ? r.json() : [])),
fetch(`${base}/gcc/${sub}/timeseries.json`).then((r) => (r.ok ? r.json() : [])),
fetch(`${base}/bands/${sub}/timeseries.json`).then((r) => (r.ok ? r.json() : [])),
]);
ndviTs = n;
gccTs = g;
bandsTs = b;
}
} catch {
ndviTs = [];
gccTs = [];
bandsTs = [];
}
drawPlots();
updateDownloadLinks();
}
@ -170,7 +193,12 @@
const el = document.getElementById("downloadLinks");
if (!el) return;
const sub = getFusionTimeseriesDir();
const base = `data/${siteName}/${season}/prepared_${strategy}/export/${sub}`;
const prep = `data/${siteName}/${season}/${getPreparedBase()}`;
if (fusionMode === "itb") {
el.innerHTML = `<a href="${prep}/gcc/${sub}/timeseries.json">[GCC JSON]</a>`;
return;
}
const base = `${prep}/export/${sub}`;
const name = `${siteName}_${season}_fusion_${strategy}_${sub}`;
el.innerHTML = `<a href="${base}/timeseries.json" download="${name}.json">[JSON]</a><a href="${base}/timeseries.csv" download="${name}.csv">[CSV]</a>`;
}
@ -185,7 +213,7 @@
const d = new Date(target.getTime() + dir * offset * 86400000);
if (d.getTime() < seasonStart || d.getTime() > seasonEnd) continue;
const ds = d.toISOString().split("T")[0].replace(/-/g, "");
const filename = `REFL_${ds}.tif`;
const filename = (fusionMode === "itb" ? "GCC_" : "REFL_") + `${ds}.tif`;
try {
const res = await fetch(`${getFusionDir()}/${filename}`, { method: "HEAD" });
if (res.ok) return filename;
@ -203,34 +231,11 @@
async function loadGeotiff(filename) {
const path = `${getFusionDir()}/${filename}`;
const tiff = await GeoTIFF.fromArrayBuffer(await (await fetch(path)).arrayBuffer());
const image = await tiff.getImage();
const rasters = await image.readRasters();
const width = image.getWidth(), height = image.getHeight();
const bbox = image.getBoundingBox();
const geoKeys = image.getGeoKeys();
const crsCode = geoKeys.ProjectedCSTypeGeoKey ? `EPSG:${geoKeys.ProjectedCSTypeGeoKey}` :
(geoKeys.GeographicTypeGeoKey !== 4326 ? `EPSG:${geoKeys.GeographicTypeGeoKey}` : "EPSG:4326");
const [blue, green, red] = [0, 1, 2].map(i => Array.from(rasters[i]));
const normalize = (arr) => {
let min = Infinity, max = -Infinity;
for (const v of arr) if (!isNaN(v) && v > 0) { min = Math.min(min, v); max = Math.max(max, v); }
return arr.map(v => Math.max(0, Math.min(255, ((v - min) / (max - min || 1)) * 255)));
};
const [rN, gN, bN] = [red, green, blue].map(normalize);
const canvas = Object.assign(document.createElement("canvas"), { width, height });
const ctx = canvas.getContext("2d");
ctx.imageSmoothingEnabled = false;
const imgData = ctx.createImageData(width, height);
for (let i = 0; i < rN.length; i++) {
const idx = i * 4;
if (rN[i] === 0 && gN[i] === 0 && bN[i] === 0) imgData.data[idx + 3] = 0;
else { imgData.data[idx] = rN[i]; imgData.data[idx + 1] = gN[i]; imgData.data[idx + 2] = bN[i]; imgData.data[idx + 3] = 255; }
}
ctx.putImageData(imgData, 0, 0);
const buf = await (await fetch(path)).arrayBuffer();
const { dataUrl, bbox, crsCode } = await geotiffToCanvasDataUrl(buf);
const bounds = crsCode === "EPSG:4326" ? [[bbox[1], bbox[0]], [bbox[3], bbox[2]]] : transformBounds(bbox, crsCode);
const dateStr = filename.replace("REFL_", "").replace(".tif", "");
return { dataUrl: canvas.toDataURL(), bounds, dateStr };
const dateStr = filename.replace(/^(REFL|GCC)_/, "").replace(".tif", "");
return { dataUrl, bounds, dateStr };
}
async function updateMap() {
@ -281,6 +286,7 @@
const params = new URLSearchParams(location.search);
params.set("site", siteName);
params.set("season", season);
params.set("mode", fusionMode);
history.replaceState({}, "", `?${params}`);
const urlDate = params.get("date");
if (urlDate) document.getElementById("dateSlider").value = daysFromDate(urlDate);
@ -328,8 +334,12 @@
document.getElementById("seasonSelect").value = initialSeason;
strategy = urlParams.get("strategy") || "aggressive";
sigma = urlParams.get("sigma") || "20";
fusionMode = urlParams.get("mode") === "itb" ? "itb" : "bti";
document.getElementById("strategySelect").value = strategy;
document.getElementById("sigmaSelect").value = sigma;
document.getElementById("fusionModeSelect").value = fusionMode;
const ml = document.getElementById("mapLabelFusion");
if (ml) ml.textContent = fusionMode === "itb" ? "Fusion GCC grayscale (closest available)" : "Fusion RGB (closest available)";
const initSite = getSiteBySitename(initialSite);
if (initSite?.geometry?.coordinates) {
@ -362,6 +372,14 @@
history.replaceState({}, "", `?${urlParams}`);
loadTimeseries(); updateMap();
});
document.getElementById("fusionModeSelect").addEventListener("change", function() {
fusionMode = this.value;
urlParams.set("mode", fusionMode);
history.replaceState({}, "", `?${urlParams}`);
const ml = document.getElementById("mapLabelFusion");
if (ml) ml.textContent = fusionMode === "itb" ? "Fusion GCC grayscale (closest available)" : "Fusion RGB (closest available)";
loadTimeseries(); updateMap();
});
await setSiteSeason(initialSite, initialSeason);
}