aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/viewer/js/layers.js
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-07 00:14:38 +0200
committergodosa <godosa@godosa.eu>2026-10-07 00:14:38 +0200
commit3443c1c65e9f1753e1e656b35d08416c1fa298f2 (patch)
tree4e43236f460145a4d75d1b4616dcb7aa6ef08f51 /viewer/js/layers.js
downloadworldmap-viewer-3443c1c65e9f1753e1e656b35d08416c1fa298f2.tar.gz
worldmap-viewer-3443c1c65e9f1753e1e656b35d08416c1fa298f2.zip
worldmap-viewer: initial public history
Diffstat (limited to 'viewer/js/layers.js')
-rw-r--r--viewer/js/layers.js84
1 files changed, 84 insertions, 0 deletions
diff --git a/viewer/js/layers.js b/viewer/js/layers.js
new file mode 100644
index 0000000..c0ab662
--- /dev/null
+++ b/viewer/js/layers.js
@@ -0,0 +1,84 @@
+// layers.js — layer textures (fetched on first use, a few kept on the GPU)
+import * as THREE from 'three';
+import { el } from './ui.js';
+
+const KEEP = 4; // base + over-layer + 2 recent; an 8192×4096 texture is ≈180 MB of GPU memory with mipmaps
+
+export class LayerStore {
+ constructor(layers, maxTextureSize, anisotropy = 1, load = null) {
+ this.layers = layers;
+ this.loadImpl = load; // tests inject a loader; the browser uses #load
+ this.pinned = new Set(); // ids wanted right now (base + over): never evicted
+ this.shown = new Set(); // ids drawn right now (may lag the wanted ones while they load): never evicted
+ this.max = maxTextureSize;
+ this.anisotropy = anisotropy;
+ this.cache = new Map(); // id → Promise<THREE.Texture>, least recently used first
+ }
+
+ get(id) {
+ return this.layers.find(l => l.id === id);
+ }
+
+ pin(ids) {
+ this.pinned = new Set(ids.filter(Boolean));
+ }
+
+ showing(ids) {
+ this.shown = new Set(ids.filter(Boolean));
+ }
+
+ dispose(keep = []) { // another era's store took over: free the GPU memory, except textures still drawn
+ for (const q of this.cache.values()) q.then(t => { if (!keep.includes(t)) t.dispose(); }, () => {});
+ this.cache.clear();
+ }
+
+ texture(id) {
+ let p = this.cache.get(id);
+ if (p) this.cache.delete(id);
+ else p = (this.loadImpl ?? (l => this.#load(l)))(this.get(id)).catch(e => { this.cache.delete(id); throw e; });
+ this.cache.set(id, p);
+ for (const [old, q] of [...this.cache]) { // oldest first; textures on screen stay
+ if (this.cache.size <= KEEP) break;
+ if (this.pinned.has(old) || this.shown.has(old) || old === id) continue;
+ this.cache.delete(old);
+ q.then(t => t.dispose(), () => {});
+ }
+ return p;
+ }
+
+ async #load(layer) {
+ const img = await new THREE.ImageLoader().loadAsync(layer.file)
+ .catch(() => { throw new Error(`could not load ${layer.file}`); });
+ let src = img;
+ if (img.width > this.max) { // GPU limit: downsample to 4096 wide
+ const w = Math.min(4096, this.max);
+ src = document.createElement('canvas');
+ src.width = w;
+ src.height = w / 2;
+ src.getContext('2d').drawImage(img, 0, 0, w, w / 2);
+ }
+ const tex = new THREE.Texture(src);
+ tex.colorSpace = THREE.NoColorSpace; // shaders pass the JPEG's sRGB bytes straight through
+ tex.anisotropy = this.anisotropy;
+ tex.needsUpdate = true;
+ return tex;
+ }
+}
+
+const UNIT_LABEL = { degC: '°C' }; // layers.json keeps the build's unit codes
+const fmtTick = v => (Math.abs(v) >= 100 ? Math.round(v).toLocaleString('en-US') : String(+v.toFixed(2)));
+
+export function renderLegend(box, layer) {
+ const L = layer.legend;
+ box.hidden = !L;
+ if (!L) return box.replaceChildren();
+ const title = el('div', { class: 'muted' }, layer.name);
+ if (L.type === 'categorical') {
+ return box.replaceChildren(title, ...L.items.map(it =>
+ el('div', {}, el('span', { class: 'sw', style: `background:${it.color}` }), it.name)));
+ }
+ const lo = L.stops[0][0], hi = L.stops.at(-1)[0];
+ const grad = L.stops.map(([v, c]) => `${c} ${(((v - lo) / (hi - lo)) * 100).toFixed(1)}%`).join(', ');
+ box.replaceChildren(title, el('div', { class: 'bar', style: `background: linear-gradient(to right, ${grad})` }),
+ el('div', { class: 'ticks' }, el('span', {}, fmtTick(lo)), el('span', {}, UNIT_LABEL[L.unit] ?? L.unit), el('span', {}, fmtTick(hi))));
+}