worldmap-viewer

git clone https://git.godosa.eu/worldmap-viewer

master

raw · 5142 bytes

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