Foo
This commit is contained in:
parent
ec3aac3ec3
commit
db14a71228
7 changed files with 374 additions and 108 deletions
57
webapp/common.js
Normal file
57
webapp/common.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/** Shared GeoTIFF → canvas for RGB (≥3 bands) or grayscale (1 band). Requires global GeoTIFF. */
|
||||
async function geotiffToCanvasDataUrl(arrayBuffer) {
|
||||
const tiff = await GeoTIFF.fromArrayBuffer(arrayBuffer);
|
||||
const image = await tiff.getImage();
|
||||
const rasters = await image.readRasters();
|
||||
const width = image.getWidth();
|
||||
const 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 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 spp = typeof image.getSamplesPerPixel === "function" ? image.getSamplesPerPixel() : 0;
|
||||
const multi = spp >= 3 || (rasters[1] != null && rasters[2] != null);
|
||||
const b0 = Array.from(rasters[0]);
|
||||
let rN, gN, bN;
|
||||
if (multi) {
|
||||
const blue = b0,
|
||||
green = Array.from(rasters[1]),
|
||||
red = Array.from(rasters[2]);
|
||||
rN = normalize(red);
|
||||
gN = normalize(green);
|
||||
bN = normalize(blue);
|
||||
} else {
|
||||
const g = normalize(b0);
|
||||
rN = g;
|
||||
gN = g;
|
||||
bN = g;
|
||||
}
|
||||
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);
|
||||
return { dataUrl: canvas.toDataURL(), bbox, crsCode };
|
||||
}
|
||||
|
|
@ -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}`;
|
||||
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() : [])
|
||||
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 = []; }
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@
|
|||
<a href="prepared.html">Prepared</a>
|
||||
<a href="fusion.html">Fusion</a>
|
||||
<a href="postprocessed.html">Postprocessed</a>
|
||||
<a href="metrics.html">Metrics</a>
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
<input type="range" id="dateSlider" min="0" max="365" value="0">
|
||||
|
|
|
|||
173
webapp/metrics.html
Normal file
173
webapp/metrics.html
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Metrics</title>
|
||||
<style>
|
||||
body { margin: 0; font-family: sans-serif; }
|
||||
.nav { margin-bottom: 15px; font-size: 14px; }
|
||||
.nav a { margin-right: 12px; color: #0066cc; text-decoration: none; }
|
||||
.nav a:hover { text-decoration: underline; }
|
||||
.nav a.active { font-weight: bold; }
|
||||
.container { max-width: 1100px; margin: 0 auto; padding: 20px; }
|
||||
.selectors { margin-bottom: 20px; }
|
||||
.selectors select { padding: 5px 10px; font-size: 14px; margin-right: 15px; }
|
||||
h1 { font-size: 22px; }
|
||||
h2 { font-size: 16px; margin-top: 24px; color: #333; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 13px; margin-bottom: 12px; }
|
||||
th, td { border: 1px solid #ccc; padding: 6px 8px; text-align: left; }
|
||||
th { background: #f5f5f5; }
|
||||
td.num { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.empty { color: #666; font-style: italic; }
|
||||
.err { color: #a00; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="nav">
|
||||
<a href="index.html">Full</a>
|
||||
<a href="preselection.html">Pre-selection</a>
|
||||
<a href="prepared.html">Prepared</a>
|
||||
<a href="fusion.html">Fusion</a>
|
||||
<a href="postprocessed.html">Postprocessed</a>
|
||||
<a href="metrics.html" class="active">Metrics</a>
|
||||
</div>
|
||||
<h1 id="siteName">Metrics</h1>
|
||||
<div class="selectors">
|
||||
<label>Site:</label>
|
||||
<select id="siteSelect"></select>
|
||||
<label>Season:</label>
|
||||
<select id="seasonSelect"></select>
|
||||
</div>
|
||||
<div id="content"></div>
|
||||
</div>
|
||||
<script>
|
||||
const METRIC_COLS = ["pearson_r", "r_squared", "rmse", "mae", "nrmse", "nse", "n_samples"];
|
||||
let siteName = "innsbruck", season = "2024";
|
||||
let availableSiteSeasons = {};
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
|
||||
async function probeMetrics(sn, s) {
|
||||
try {
|
||||
const res = await fetch(`data/${sn}/${s}/metrics.json`, { method: "HEAD" });
|
||||
return res.ok;
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
function fmt(v) {
|
||||
if (v == null || typeof v !== "number") return "—";
|
||||
return Number.isInteger(v) ? String(v) : v.toFixed(4);
|
||||
}
|
||||
|
||||
function tableSection(title, obj) {
|
||||
if (!obj || typeof obj !== "object" || !Object.keys(obj).length) {
|
||||
return `<h2>${title}</h2><p class="empty">No data</p>`;
|
||||
}
|
||||
const keys = Object.keys(obj).sort();
|
||||
let head = `<tr><th>Scenario</th>${METRIC_COLS.map((c) => `<th>${c}</th>`).join("")}</tr>`;
|
||||
const rows = keys.map((k) => {
|
||||
const m = obj[k] || {};
|
||||
return `<tr><td>${k}</td>${METRIC_COLS.map((c) => `<td class="num">${fmt(m[c])}</td>`).join("")}</tr>`;
|
||||
}).join("");
|
||||
return `<h2>${title}</h2><table>${head}${rows}</table>`;
|
||||
}
|
||||
|
||||
function render(data) {
|
||||
const el = document.getElementById("content");
|
||||
if (!data) {
|
||||
el.innerHTML = `<p class="err">Could not load metrics.json</p>`;
|
||||
return;
|
||||
}
|
||||
let html = "";
|
||||
if (data.phenocam_stats) {
|
||||
html += `<h2>PhenoCam</h2><table><tr><th>mean</th><th>std</th><th>min</th><th>max</th><th>n</th></tr><tr>`;
|
||||
const p = data.phenocam_stats;
|
||||
html += `<td class="num">${fmt(p.mean)}</td><td class="num">${fmt(p.std)}</td><td class="num">${fmt(p.min)}</td><td class="num">${fmt(p.max)}</td><td class="num">${fmt(p.n_samples)}</td></tr></table>`;
|
||||
}
|
||||
if (data.baseline && data.baseline.s2) {
|
||||
html += `<h2>Baseline S2 (temporal)</h2><table><tr>${METRIC_COLS.map((c) => `<th>${c}</th>`).join("")}</tr><tr>`;
|
||||
const b = data.baseline.s2;
|
||||
html += METRIC_COLS.map((c) => `<td class="num">${fmt(b[c])}</td>`).join("") + "</tr></table>";
|
||||
}
|
||||
html += tableSection("Temporal (vs PhenoCam)", data.temporal);
|
||||
html += tableSection("Spatial (3×3 fusion mean vs PhenoCam)", data.spatial);
|
||||
if (data.summary) {
|
||||
html += `<h2>Summary</h2><pre style="font-size:13px;background:#f9f9f9;padding:10px;">${JSON.stringify(data.summary, null, 2)}</pre>`;
|
||||
}
|
||||
el.innerHTML = html || `<p class="empty">Empty metrics file</p>`;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const res = await fetch(`data/${siteName}/${season}/metrics.json`);
|
||||
render(res.ok ? await res.json() : null);
|
||||
} catch {
|
||||
render(null);
|
||||
}
|
||||
const site = window.sitesData?.features?.find((f) => f.properties?.sitename === siteName);
|
||||
document.getElementById("siteName").textContent = (site?.properties?.description || siteName) + " — " + season;
|
||||
urlParams.set("site", siteName);
|
||||
urlParams.set("season", season);
|
||||
history.replaceState({}, "", `?${urlParams}`);
|
||||
}
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
const res = await fetch("data/sites.geojson");
|
||||
window.sitesData = res.ok ? await res.json() : { features: [] };
|
||||
} catch { window.sitesData = { features: [] }; }
|
||||
const features = window.sitesData.features || [];
|
||||
for (const f of features) {
|
||||
const sn = f.properties?.sitename;
|
||||
if (!sn) continue;
|
||||
const seasonsFromGeo = f.properties?.seasons ? Object.keys(f.properties.seasons).sort() : [];
|
||||
const withData = [];
|
||||
for (const s of seasonsFromGeo) {
|
||||
if (await probeMetrics(sn, s)) withData.push(s);
|
||||
}
|
||||
if (withData.length) availableSiteSeasons[sn] = withData;
|
||||
}
|
||||
const availableSites = Object.keys(availableSiteSeasons);
|
||||
const siteSelect = document.getElementById("siteSelect");
|
||||
siteSelect.innerHTML = "";
|
||||
(availableSites.length ? availableSites.sort() : ["innsbruck"]).forEach((sn) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = sn;
|
||||
opt.textContent = sn;
|
||||
siteSelect.appendChild(opt);
|
||||
if (!availableSiteSeasons[sn]) availableSiteSeasons[sn] = ["2024"];
|
||||
});
|
||||
const urlSite = urlParams.get("site");
|
||||
const urlSeason = urlParams.get("season");
|
||||
const initialSite = urlSite && availableSiteSeasons[urlSite] ? urlSite : availableSites[0] || "innsbruck";
|
||||
const initialSeason =
|
||||
urlSeason && (availableSiteSeasons[initialSite] || []).includes(urlSeason)
|
||||
? urlSeason
|
||||
: (availableSiteSeasons[initialSite] || [])[0] || "2024";
|
||||
siteSelect.value = initialSite;
|
||||
document.getElementById("seasonSelect").innerHTML = (availableSiteSeasons[initialSite] || [])
|
||||
.map((s) => `<option value="${s}">${s}</option>`)
|
||||
.join("");
|
||||
document.getElementById("seasonSelect").value = initialSeason;
|
||||
siteName = initialSite;
|
||||
season = initialSeason;
|
||||
|
||||
siteSelect.addEventListener("change", function () {
|
||||
const sn = this.value;
|
||||
const seas = availableSiteSeasons[sn] || [];
|
||||
document.getElementById("seasonSelect").innerHTML = seas.map((s) => `<option value="${s}">${s}</option>`).join("");
|
||||
document.getElementById("seasonSelect").value = seas[0] || "2024";
|
||||
siteName = sn;
|
||||
season = document.getElementById("seasonSelect").value;
|
||||
load();
|
||||
});
|
||||
document.getElementById("seasonSelect").addEventListener("change", function () {
|
||||
season = this.value;
|
||||
load();
|
||||
});
|
||||
await load();
|
||||
}
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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">Fusion</a>
|
||||
<a href="postprocessed.html" class="active">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>
|
||||
|
|
@ -66,6 +68,11 @@
|
|||
<option value="fusion">Fusion</option>
|
||||
<option value="s3">S3</option>
|
||||
</select>
|
||||
<label>Mode:</label>
|
||||
<select id="fusionModeSelect" title="BtI vs ItB processed paths">
|
||||
<option value="bti">BtI</option>
|
||||
<option value="itb">ItB</option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="range" id="dateSlider" min="0" max="365" value="0">
|
||||
<div id="dateDisplay">2024-01-01</div>
|
||||
|
|
@ -87,7 +94,7 @@
|
|||
proj4.defs("EPSG:4326", "+proj=longlat +datum=WGS84 +no_defs");
|
||||
|
||||
let siteName = "innsbruck", season = "2024";
|
||||
let strategy = "aggressive", sigma = "20", source = "s2";
|
||||
let strategy = "aggressive", sigma = "20", source = "s2", fusionMode = "bti";
|
||||
let sitePosition = [47.116171, 11.320308];
|
||||
let start = new Date(2024, 0, 1);
|
||||
let availableSiteSeasons = {};
|
||||
|
|
@ -105,18 +112,26 @@
|
|||
};
|
||||
|
||||
function getProcessedPath() {
|
||||
return `data/${siteName}/${season}/processed_${strategy}_sigma${sigma}`;
|
||||
const mid = fusionMode === "itb" ? `processed_${strategy}_itb_sigma${sigma}` : `processed_${strategy}_sigma${sigma}`;
|
||||
return `data/${siteName}/${season}/${mid}`;
|
||||
}
|
||||
|
||||
async function loadTimeseries() {
|
||||
const base = getProcessedPath();
|
||||
try {
|
||||
const [n, g, b] = await Promise.all([
|
||||
fetch(`${getProcessedPath()}/ndvi/${source}/timeseries.json`).then(r => r.ok ? r.json() : []),
|
||||
fetch(`${getProcessedPath()}/gcc/${source}/timeseries.json`).then(r => r.ok ? r.json() : []),
|
||||
fetch(`${getProcessedPath()}/bands/${source}/timeseries.json`).then(r => r.ok ? r.json() : [])
|
||||
fetch(`${base}/ndvi/${source}/timeseries.json`).then((r) => (r.ok ? r.json() : [])),
|
||||
fetch(`${base}/gcc/${source}/timeseries.json`).then((r) => (r.ok ? r.json() : [])),
|
||||
fetch(`${base}/bands/${source}/timeseries.json`).then((r) => (r.ok ? r.json() : [])),
|
||||
]);
|
||||
ndviTs = n; gccTs = g; bandsTs = b;
|
||||
} catch { ndviTs = []; gccTs = []; bandsTs = []; }
|
||||
ndviTs = n;
|
||||
gccTs = g;
|
||||
bandsTs = b;
|
||||
} catch {
|
||||
ndviTs = [];
|
||||
gccTs = [];
|
||||
bandsTs = [];
|
||||
}
|
||||
drawPlots();
|
||||
updateDownloadLinks();
|
||||
}
|
||||
|
|
@ -167,7 +182,12 @@
|
|||
function updateDownloadLinks() {
|
||||
const el = document.getElementById("downloadLinks");
|
||||
if (!el) return;
|
||||
const base = `data/${siteName}/${season}/processed_${strategy}_sigma${sigma}/export/${source}`;
|
||||
const root = getProcessedPath();
|
||||
if (fusionMode === "itb") {
|
||||
el.innerHTML = `<a href="${root}/gcc/${source}/timeseries.json">[GCC JSON]</a>`;
|
||||
return;
|
||||
}
|
||||
const base = `${root}/export/${source}`;
|
||||
const name = `${siteName}_${season}_postprocessed_${strategy}_sigma${sigma}_${source}`;
|
||||
el.innerHTML = `<a href="${base}/timeseries.json" download="${name}.json">[JSON]</a><a href="${base}/timeseries.csv" download="${name}.csv">[CSV]</a>`;
|
||||
}
|
||||
|
|
@ -200,34 +220,11 @@
|
|||
|
||||
async function loadGeotiff(filename) {
|
||||
const path = `${getProcessedPath()}/${source}/${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("_0.geotiff", "");
|
||||
return { dataUrl: canvas.toDataURL(), bounds, dateStr };
|
||||
return { dataUrl, bounds, dateStr };
|
||||
}
|
||||
|
||||
async function updateMap() {
|
||||
|
|
@ -278,6 +275,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);
|
||||
|
|
@ -326,9 +324,11 @@
|
|||
strategy = urlParams.get("strategy") || "aggressive";
|
||||
sigma = urlParams.get("sigma") || "20";
|
||||
source = urlParams.get("source") || "s2";
|
||||
fusionMode = urlParams.get("mode") === "itb" ? "itb" : "bti";
|
||||
document.getElementById("strategySelect").value = strategy;
|
||||
document.getElementById("sigmaSelect").value = sigma;
|
||||
document.getElementById("sourceSelect").value = source;
|
||||
document.getElementById("fusionModeSelect").value = fusionMode;
|
||||
|
||||
const initSite = getSiteBySitename(initialSite);
|
||||
if (initSite?.geometry?.coordinates) {
|
||||
|
|
@ -367,6 +367,12 @@
|
|||
history.replaceState({}, "", `?${urlParams}`);
|
||||
loadTimeseries(); updateMap();
|
||||
});
|
||||
document.getElementById("fusionModeSelect").addEventListener("change", function() {
|
||||
fusionMode = this.value;
|
||||
urlParams.set("mode", fusionMode);
|
||||
history.replaceState({}, "", `?${urlParams}`);
|
||||
loadTimeseries(); updateMap();
|
||||
});
|
||||
|
||||
await setSiteSeason(initialSite, initialSeason);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" class="active">Prepared</a>
|
||||
<a href="fusion.html">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>
|
||||
|
|
@ -57,8 +59,13 @@
|
|||
</select>
|
||||
<label>Source:</label>
|
||||
<select id="sourceSelect">
|
||||
<option value="s2">S2 REFL</option>
|
||||
<option value="s3">S3 Composite</option>
|
||||
<option value="s2">S2</option>
|
||||
<option value="s3">S3</option>
|
||||
</select>
|
||||
<label>Mode:</label>
|
||||
<select id="fusionModeSelect" title="BtI = REFL/composite; ItB = GCC rasters">
|
||||
<option value="bti">BtI</option>
|
||||
<option value="itb">ItB</option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="range" id="dateSlider" min="0" max="365" value="0">
|
||||
|
|
@ -81,7 +88,7 @@
|
|||
proj4.defs("EPSG:4326", "+proj=longlat +datum=WGS84 +no_defs");
|
||||
|
||||
let siteName = "innsbruck", season = "2024";
|
||||
let strategy = "aggressive", source = "s2";
|
||||
let strategy = "aggressive", source = "s2", fusionMode = "bti";
|
||||
let sitePosition = [47.116171, 11.320308];
|
||||
let start = new Date(2024, 0, 1);
|
||||
let availableSiteSeasons = {};
|
||||
|
|
@ -99,7 +106,8 @@
|
|||
};
|
||||
|
||||
function getPreparedPath() {
|
||||
return `data/${siteName}/${season}/prepared_${strategy}`;
|
||||
const mid = fusionMode === "itb" ? `prepared_${strategy}_itb` : `prepared_${strategy}`;
|
||||
return `data/${siteName}/${season}/${mid}`;
|
||||
}
|
||||
|
||||
async function loadTimeseries() {
|
||||
|
|
@ -161,7 +169,12 @@
|
|||
function updateDownloadLinks() {
|
||||
const el = document.getElementById("downloadLinks");
|
||||
if (!el) return;
|
||||
const base = `data/${siteName}/${season}/prepared_${strategy}/export/${source}`;
|
||||
const root = getPreparedPath();
|
||||
if (fusionMode === "itb") {
|
||||
el.innerHTML = `<a href="${root}/gcc/${source}/timeseries.json">[GCC JSON]</a>`;
|
||||
return;
|
||||
}
|
||||
const base = `${root}/export/${source}`;
|
||||
const name = `${siteName}_${season}_prepared_${strategy}_${source}`;
|
||||
el.innerHTML = `<a href="${base}/timeseries.json" download="${name}.json">[JSON]</a><a href="${base}/timeseries.csv" download="${name}.csv">[CSV]</a>`;
|
||||
}
|
||||
|
|
@ -176,7 +189,12 @@
|
|||
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 = source === "s2" ? `S2A_MSIL2A_${ds}_REFL.tif` : `composite_${ds}.tif`;
|
||||
const filename =
|
||||
source === "s2"
|
||||
? fusionMode === "itb"
|
||||
? `S2A_MSIL2A_${ds}_GCC.tif`
|
||||
: `S2A_MSIL2A_${ds}_REFL.tif`
|
||||
: `composite_${ds}.tif`;
|
||||
try {
|
||||
const res = await fetch(`${getPreparedPath()}/${source}/${filename}`, { method: "HEAD" });
|
||||
if (res.ok) return filename;
|
||||
|
|
@ -194,33 +212,12 @@
|
|||
|
||||
async function loadGeotiff(filename) {
|
||||
const path = `${getPreparedPath()}/${source}/${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);
|
||||
return { dataUrl: canvas.toDataURL(), bounds, dateStr: filename.includes("composite") ? filename.replace("composite_", "").replace(".tif", "") : filename.replace("S2A_MSIL2A_", "").replace("_REFL.tif", "") };
|
||||
const m = filename.match(/(\d{8})/);
|
||||
const dateStr = m ? m[1] : "";
|
||||
return { dataUrl, bounds, dateStr };
|
||||
}
|
||||
|
||||
async function updateMap() {
|
||||
|
|
@ -271,6 +268,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);
|
||||
|
|
@ -318,8 +316,12 @@
|
|||
document.getElementById("seasonSelect").value = initialSeason;
|
||||
strategy = urlParams.get("strategy") || "aggressive";
|
||||
source = urlParams.get("source") || "s2";
|
||||
fusionMode = urlParams.get("mode") === "itb" ? "itb" : "bti";
|
||||
document.getElementById("strategySelect").value = strategy;
|
||||
document.getElementById("sourceSelect").value = source;
|
||||
document.getElementById("fusionModeSelect").value = fusionMode;
|
||||
const ml = document.getElementById("mapLabel");
|
||||
if (ml) ml.textContent = fusionMode === "itb" ? "Prepared GCC grayscale / S3 (closest available)" : "Prepared RGB (closest available)";
|
||||
|
||||
const initSite = getSiteBySitename(initialSite);
|
||||
if (initSite?.geometry?.coordinates) {
|
||||
|
|
@ -352,6 +354,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("mapLabel");
|
||||
if (ml) ml.textContent = fusionMode === "itb" ? "Prepared GCC grayscale / S3 (closest available)" : "Prepared RGB (closest available)";
|
||||
loadTimeseries(); updateMap();
|
||||
});
|
||||
|
||||
await setSiteSeason(initialSite, initialSeason);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
<a href="prepared.html">Prepared</a>
|
||||
<a href="fusion.html">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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue