From 3443c1c65e9f1753e1e656b35d08416c1fa298f2 Mon Sep 17 00:00:00 2001 From: godosa Date: Wed, 7 Oct 2026 00:14:38 +0200 Subject: worldmap-viewer: initial public history --- viewer/js/regionspanel.js | 125 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 viewer/js/regionspanel.js (limited to 'viewer/js/regionspanel.js') diff --git a/viewer/js/regionspanel.js b/viewer/js/regionspanel.js new file mode 100644 index 0000000..e0e7078 --- /dev/null +++ b/viewer/js/regionspanel.js @@ -0,0 +1,125 @@ +// regionspanel.js — the Regions tab: list refined regions, draw a new outline by clicking the map, save, delete, +// and show the background refinement's progress +import { api, el, toast } from './ui.js'; +import { WARN_KM2, crossesItself, outlineAreaKm2, regionPayload, statusText } from './regions.js'; + +export class RegionsPanel { + constructor(box, { onDraft, onDrawing, onRegions, onStatus, eraQuery = () => '' }) { + Object.assign(this, { box, onDraft, onDrawing, onRegions, onStatus, eraQuery }); + this.regions = []; + this.status = null; + this.points = null; // the outline being drawn + this.closed = false; + this.timer = null; + } + + async load() { + try { + const q = this.eraQuery(), r = await api(`api/regions${q}`); + this.regions = r.regions; + this.#status(r.status, r.tiles, q); + } catch (e) { + toast(`Regions: ${e.message}`); + } + this.onRegions(this.regions); + this.render(); + } + + #status(s, tiles, q = this.eraQuery()) { + const was = this.status?.state; + this.status = s; + this.onStatus(s, tiles, was, q); + clearTimeout(this.timer); + const saving = s?.tiles && s.tiles.done < s.tiles.total; // tiles being saved ahead: keep the percentage fresh + const busy = s && ['queued', 'building'].includes(s.state); + this.timer = setTimeout(() => this.poll(), busy ? 700 : saving ? 3000 : 30000); // idle: another tab may build + } + + async poll() { + try { + const q = this.eraQuery(), r = await api(`api/regions${q}`); + this.regions = r.regions; + this.#status(r.status, r.tiles, q); + this.onRegions(this.regions); + if (!this.points) this.render(); // never wipe a name being typed + } catch { + this.timer = setTimeout(() => this.poll(), 2000); + } + } + + startDraw() { + this.points = []; + this.closed = false; + this.onDrawing(true); + this.render(); + } + + cancel() { + this.points = null; + this.closed = false; + this.onDraft(null); + this.onDrawing(false); + this.render(); + } + + addPoint(p, action) { // action: 'add' | 'close' | 'ignore' (regions.outlineClick) + if (!this.points || this.closed || action === 'ignore') return; + if (action === 'close' && this.points.length >= 3) { + this.closed = true; + this.onDrawing(false); + } else this.points.push(p); + this.onDraft({ points: this.points, closed: this.closed }); + this.render(); + } + + async save(name) { + if (crossesItself(this.points)) return toast('The outline crosses itself: redraw it'); + const km2 = outlineAreaKm2(this.points); + if (km2 > WARN_KM2 && !confirm(`This region is ≈ ${Math.round(km2).toLocaleString()} km². Regions over ` + + `${WARN_KM2.toLocaleString()} km² take minutes to refine and a lot of memory. Refine it anyway?`)) return; + try { + await api('api/regions', { method: 'POST', body: regionPayload(name, this.points) }); + this.points = null; + this.closed = false; + this.onDraft(null); + await this.load(); + } catch (e) { + toast(`Could not save the region: ${e.message}`); + } + } + + async remove(r) { + if (!confirm(`Delete region “${r.name}”? Its refined terrain goes too.`)) return; + try { + await api(`api/regions/${r.id}`, { method: 'DELETE' }); + await this.load(); + } catch (e) { + toast(`Could not delete the region: ${e.message}`); + } + } + + render() { + const st = el('p', { class: this.status?.state === 'failed' ? 'warn' : 'muted' }, statusText(this.status)); + if (this.points) { + const name = el('input', { required: true, maxlength: 80, placeholder: 'Region name' }); + const form = el('form', { class: 'region' }, + el('p', { class: 'muted' }, this.closed ? `${this.points.length} points — name it and save` + : `Click the map to outline the region (${this.points.length} points); click the first point to close. Esc cancels.`), + this.closed ? el('label', {}, 'Name', name) : null, + el('div', {}, this.closed ? el('button', { type: 'submit' }, 'Save and refine') : null, ' ', + el('button', { type: 'button', onclick: () => this.cancel() }, 'Cancel'))); + form.addEventListener('submit', e => { e.preventDefault(); this.save(name.value); }); + if (this.closed) queueMicrotask(() => name.focus()); + return this.box.replaceChildren(form, st); + } + const item = r => el('div', { class: 'pin-item' }, el('b', {}, r.name), el('br'), + el('span', { class: 'muted' }, `${Math.round(r.area_km2 + ?? outlineAreaKm2(r.outline.map(([lat, lon]) => ({ lat, lon })))).toLocaleString()} km² · ` + + (r.refined ? 'refined' : 'not refined yet')), ' ', + el('button', { onclick: () => this.remove(r) }, 'Delete')); + this.box.replaceChildren( + el('div', {}, el('button', { onclick: () => this.startDraw() }, '+ Draw region')), + el('p', { class: 'muted' }, 'Refined regions re-run erosion, rivers, lakes and life zones at ≈ 5 km (generated terrain: idea).'), + st, ...(this.regions.length ? this.regions.map(item) : [el('p', { class: 'muted' }, 'No regions yet.')])); + } +} -- cgit