worldmap-viewer

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

master

raw · 4687 bytes

// exportpanel.js — the Export tab: freeze the square around the map centre into game-engine files (map/exports/)
import { api, el, toast } from './ui.js';
import { exportSizeMB, exportStatusText, samplesFor, squareOutline } from './exports.js';
import { fmtLatLon } from './units.js';

const SIZES = [5, 10, 20, 40, 80, 160];            // km
const RESOLUTIONS = [1, 2, 5, 10, 20, 50, 100];    // m per sample
const MAX_SAMPLES = 8193;                          // as the server (serve.EXPORT_MAX_SAMPLES)

export class ExportPanel {
  constructor(box, { getCenter, onSquare, getEra = () => undefined }) {
    Object.assign(this, { box, getCenter, onSquare, getEra });
    this.size = 20;
    this.res = 10;
    this.list = [];
    this.status = null;
    this.visible = false;
    this.timer = null;
  }

  async load() {
    try {
      const r = await api('api/exports');
      this.list = r.exports;
      this.status = r.status;
    } catch (e) {
      toast(`Exports: ${e.message}`);
    }
    clearTimeout(this.timer);
    if (this.status?.state === 'running') this.timer = setTimeout(() => this.load(), 700);
    if (this.statusEl) this.refresh();                        // a poll: never rebuild what is being typed or picked
    else this.render();
  }

  refresh() {   // the parts that change while an export runs: status line, button, list
    const busy = this.status?.state === 'running', tooMany = samplesFor(this.size, this.res) > MAX_SAMPLES;
    this.statusEl.textContent = exportStatusText(this.status);
    this.statusEl.setAttribute('class', this.status?.state === 'failed' ? 'warn' : 'muted');
    if (busy || tooMany) this.button.setAttribute('disabled', 'true');
    else this.button.removeAttribute?.('disabled');
    this.listEl.replaceChildren(...(this.list.length ? this.list.map(e => this.#item(e)) : [el('p', { class: 'muted' }, 'No exports yet.')]));
  }

  #item(e) {
    return el('div', { class: 'pin-item' }, el('b', {}, e.name), el('br'),
      el('span', { class: 'muted' }, `${(e.size_m / 1000).toLocaleString()} km · ${e.res_m} m · ${e.samples} × ${e.samples}`), el('br'),
      el('span', { class: 'muted small' }, e.path));
  }

  show() {
    this.visible = true;
    this.render();
    this.update();
    this.load();
  }

  hide() {
    this.visible = false;
    this.onSquare(null);
  }

  update() {   // the view moved or the size changed: the square and the centre line follow
    if (!this.visible) return;
    const c = this.getCenter();
    this.onSquare(squareOutline(c.lat, c.lon, this.size));
    if (this.centre) this.centre.textContent = `Centre (map centre): ${fmtLatLon(c.lat, c.lon)}`;
  }

  async start(name) {
    const c = this.getCenter();
    try {
      await api('api/exports', { method: 'POST', body: { lat: c.lat, lon: c.lon, size_km: this.size, res_m: this.res,
        era: this.getEra() || undefined, ...(name.trim() ? { name: name.trim() } : {}) } });
    } catch (e) {
      return toast(`Could not export: ${e.message}`);
    }
    await this.load();
  }

  render() {
    const n = samplesFor(this.size, this.res), tooMany = n > MAX_SAMPLES;
    const select = (key, values, unit) => {
      const s = el('select', { name: key }, ...values.map(v => el('option', { value: String(v), selected: v === this[key] ? true : null }, `${v} ${unit}`)));
      s.addEventListener('change', () => { this[key] = Number(s.value); this.render(); this.update(); });
      return s;
    };
    const name = el('input', { maxlength: 64, placeholder: 'name (optional: a–z 0–9 . _ -)' });
    name.addEventListener('input', () => { const v = name.value.toLowerCase().replace(/[^a-z0-9._-]/g, '-'); if (v !== name.value) name.value = v; });
    this.centre = el('p', { class: 'muted' }, '');
    this.button = el('button', { onclick: () => this.start(name.value) }, 'Export here (map centre)');
    this.statusEl = el('p', { class: 'muted' }, '');
    this.listEl = el('div', {});
    this.box.replaceChildren(
      el('p', { class: 'muted' }, 'Freeze the square around the map centre into files for a game engine: heights, water, '
        + 'biomes, rivers, pins (map/exports/, see its README.txt). Generated terrain: idea.'),
      el('label', { class: 'row' }, 'Size ', select('size', SIZES, 'km'), ' Resolution ', select('res', RESOLUTIONS, 'm')),
      el('p', { class: tooMany ? 'warn' : 'muted' }, `${n} × ${n} samples · ≈ ${Math.round(exportSizeMB(n))} MB`
        + (tooMany ? ' — too many: choose a coarser resolution or a smaller square' : '')),
      this.centre, el('label', { class: 'row' }, name), el('div', {}, this.button), this.statusEl, this.listEl);
    this.refresh();
    this.update();
  }
}