raw · 6202 bytes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | // overlays.js — vector overlays (GeoJSON, graticule, measure path) and HTML markers for the active view import { linesFromGeoJSON, graticuleLines } from './overlay_geom.js'; import { api, el, toast } from './ui.js'; import { riverMinOrder, riverStats } from './riverlod.js'; export const COLORS = { coast: 0xf2efe6, lakes: 0x8fd0ff, rivers: 0x5ab4ff, plates: 0xff6a4d, graticule: 0x8a93a8, measure: 0x7fd4ff, regions: 0xffd166 }; export class Overlays { constructor(defs, markerBox, onChange = () => {}) { this.defs = defs; // [{id, name, file}] from /api/meta this.onChange = onChange; // lines changed (often after an await): the app must redraw this.box = markerBox; this.on = new Set(); this.cache = new Map(); // id → Promise<Line[]> (rivers: {all, big}) this.extra = new Map(); // always-shown line sets, e.g. the measure path this.markers = []; this.view = null; this.kmPerPx = null; // rivers: only orders that stay apart on screen at this scale (riverlod.js) this.riverOrder = null; this.cellKm = 10; // the build's cell spacing (setCellKm) this.grat = null; this.suppressed = new Set(); // enabled but hidden (coast/lakes under deep-zoom tiles) this.regionLines = []; // refined-region outlines (from /api/regions) } list() { return [...this.defs.map(({ id, name }) => ({ id, name })), { id: 'graticule', name: 'Graticule 30°' }, { id: 'regions', name: 'Refined regions' }, { id: 'vents', name: 'Vents (refined areas)' }, { id: 'pins', name: 'Pins' }]; } setDefs(defs) { // another era: its own coast, lakes, rivers… (loaded again when shown) this.defs = defs; this.cache.clear(); for (const id of this.#ids()) this.#push(id); } attach(view) { this.view = view; for (const id of this.#ids()) this.#push(id); this.#buildMarkers(); } setEnabled(ids) { const before = this.on; this.on = new Set(ids); for (const id of this.#ids()) if (before.has(id) !== this.on.has(id)) this.#push(id); this.updateMarkers(); } setCellKm(km) { this.cellKm = km; } setScale(kmPerPx) { // the map's scale changed: rivers may gain or lose an order this.kmPerPx = kmPerPx; const data = this.cache.get('rivers')?.ready; if (!data || !this.on.has('rivers')) return; if (riverMinOrder(kmPerPx, data.stats, this.cellKm) !== this.riverOrder) this.#push('rivers'); } setSuppressed(ids) { const next = new Set(ids); const changed = [...next, ...this.suppressed].filter(id => next.has(id) !== this.suppressed.has(id)); this.suppressed = next; for (const id of changed) this.#push(id); } setRegions(lines) { this.regionLines = lines; this.#push('regions'); } removeExtra(id) { if (!this.extra.delete(id)) return; this.view?.removeLines(id); this.onChange(); } setExtra(id, lines, color = COLORS.measure) { this.extra.set(id, { lines, color }); this.#push(id); } setMarkers(kind, list) { const old = this.markers.filter(m => m.kind === kind); if (old.length === list.length && old.every((m, i) => m.label === list[i].label && m.onClick === list[i].onClick && m.title === list[i].title && m.cls === list[i].cls)) { old.forEach((m, i) => { m.lat = list[i].lat; m.lon = list[i].lon; }); // same markers moved: keep the DOM return this.updateMarkers(); } this.markers = this.markers.filter(m => m.kind !== kind).concat(list.map(m => ({ ...m, kind }))); this.#buildMarkers(); } updateMarkers() { for (const m of this.markers) { const shown = (m.kind !== 'pin' || this.on.has('pins')) && (m.kind !== 'vent' || this.on.has('vents')); const p = this.view && shown ? this.view.toScreen(m.lat, m.lon) : null; m.el.style.display = p ? '' : 'none'; if (p) m.el.style.transform = `translate(${p.x}px, ${p.y}px) translate(-50%, -50%)`; } } #ids() { return [...this.defs.map(d => d.id), 'graticule', 'regions', ...this.extra.keys()]; } #buildMarkers() { for (const m of this.markers) { m.el ??= el('div', { class: `marker ${m.kind}${m.cls ? ` ${m.cls}` : ''}${m.onClick ? ' clickable' : ''}`, title: m.title, onclick: m.onClick }, el('div', { class: 'dot' }), m.label ? el('div', { class: 'label' }, m.label) : null); } this.box.replaceChildren(...this.markers.map(m => m.el)); this.updateMarkers(); } async #push(id) { const view = this.view; if (!view) return; if (this.extra.has(id)) view.setLines(id, this.extra.get(id).lines, this.extra.get(id).color); else if (!this.on.has(id) || this.suppressed.has(id)) view.removeLines(id); else if (id === 'graticule') view.setLines(id, (this.grat ??= graticuleLines(30)), COLORS.graticule); else if (id === 'regions') view.setLines(id, this.regionLines, COLORS.regions); else return this.#pushData(id, view); this.onChange(); } async #pushData(id, view) { try { const data = await this.#load(id); if (this.view !== view || !this.on.has(id) || this.suppressed.has(id)) return; // switched, toggled or hidden meanwhile let lines = data; if (id === 'rivers') { this.riverOrder = riverMinOrder(this.kmPerPx, data.stats, this.cellKm); lines = data.from(this.riverOrder); } view.setLines(id, lines, COLORS[id] ?? 0xffffff); this.onChange(); } catch (e) { toast(`Overlay “${id}”: ${e.message}`); } } #load(id) { if (!this.cache.has(id)) { const def = this.defs.find(d => d.id === id); const p = api(def.file).then(fc => (id === 'rivers' ? riverSets(fc) : linesFromGeoJSON(fc))); if (id === 'rivers') p.then(d => { p.ready = d; }, () => { }); p.catch(() => this.cache.delete(id)); this.cache.set(id, p); } return this.cache.get(id); } } function riverSets(fc) { // the rivers overlay: its statistics, and the lines of order ≥ k (built when first asked) const stats = riverStats(fc), sets = new Map(); return { stats, from: k => sets.get(k) ?? sets.set(k, linesFromGeoJSON(fc, q => (q.order ?? 1) >= k)).get(k) }; } |