// 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.')])); } }