raw · 4687 bytes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | // 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(); } } |