aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/viewer/js/regionspanel.js
diff options
context:
space:
mode:
Diffstat (limited to 'viewer/js/regionspanel.js')
-rw-r--r--viewer/js/regionspanel.js125
1 files changed, 125 insertions, 0 deletions
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.')]));
+ }
+}