worldmap-viewer

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

master

raw · 4089 bytes

// measure.js — great-circle measuring path (pure) and its panel
import { distanceKm, polygonAreaKm2 } from './geo.js';
import { fmtDistance, fmtArea, fmtWalk, fmtElev } from './units.js';
import { el, svg } from './ui.js';
import { profileModel, sampleAt } from './profile.js';

export class MeasurePath {
  constructor() {
    this.points = [];
    this.closed = false;
  }

  add(p) {
    if (this.closed) this.clear();
    const last = this.points.at(-1);
    if (last && last.lat === p.lat && last.lon === p.lon) return;   // double-click lands twice
    this.points.push({ lat: p.lat, lon: p.lon });
  }

  undo() {
    if (this.closed) this.closed = false;
    else this.points.pop();
  }

  clear() {
    this.points = [];
    this.closed = false;
  }

  close() {
    if (this.points.length >= 3) this.closed = true;
    return this.closed;
  }

  #ring() { return this.closed ? [...this.points, this.points[0]] : this.points; }

  segmentsKm() {
    const r = this.#ring();
    return r.slice(1).map((p, i) => distanceKm(r[i], p));
  }

  totalKm() { return this.segmentsKm().reduce((a, b) => a + b, 0); }
  areaKm2() { return this.closed ? polygonAreaKm2(this.points) : null; }
  lines() { return this.points.length < 2 ? [] : [[...this.#ring()]]; }
}

export function renderMeasure(box, path, { undo, close, clear }) {
  const segs = path.segmentsKm(), n = path.points.length;
  const profile = el('div', { class: 'profile-box' });
  box.replaceChildren(...[   // replaceChildren would print null as text: drop the absent parts
    el('p', { class: 'muted' }, 'Click the map to add points; Esc clears.'),
    el('div', {}, el('button', { onclick: undo, disabled: !n }, 'Undo'), ' ',
      el('button', { onclick: close, disabled: n < 3 || path.closed }, 'Close for area'), ' ',
      el('button', { onclick: clear, disabled: !n }, 'Clear')),
    segs.length ? el('ol', {}, segs.map(km => el('li', {}, fmtDistance(km)))) : null,
    segs.length ? el('p', {}, el('b', {}, 'Total '), fmtDistance(path.totalKm()), el('br'),
      el('span', { class: 'muted' }, `${fmtWalk(path.totalKm())} (30 km a day)`)) : null,
    path.closed ? el('p', {}, el('b', {}, 'Area '), fmtArea(path.areaKm2())) : null,
    profile,
  ].filter(Boolean));
  return profile;
}

const CW = 320, CH = 130;
const int = v => Math.round(v).toLocaleString('en-US');

export function renderProfile(box, prof, onHover) {
  const s = prof.samples, m = profileModel(s, CW, CH);
  const cursor = svg('line', { class: 'cursor', x1: 0, x2: 0, y1: 8, y2: CH - 18, visibility: 'hidden' });
  const readout = el('div', { class: 'readout muted' }, ' ');
  const chart = svg('svg', { class: 'profile', viewBox: `0 0 ${CW} ${CH}`, width: CW, height: CH },
    ...m.water.map(([a, b]) => svg('rect', { class: 'water', x: a, y: 8, width: Math.max(1, b - a), height: CH - 26 })),
    svg('line', { class: 'sea', x1: m.X(0), x2: m.X(m.L), y1: m.seaY, y2: m.seaY }),
    svg('path', { class: 'area', d: m.area }),
    svg('path', { class: 'line', d: m.line }),
    svg('text', { x: 2, y: 14 }, `${int(m.hi)} m`),
    svg('text', { x: 2, y: CH - 20 }, `${int(m.lo)} m`),
    svg('text', { x: CW - 4, y: CH - 4, 'text-anchor': 'end' }, fmtDistance(m.L)),
    cursor);
  chart.addEventListener('pointermove', e => {
    const r = chart.getBoundingClientRect();
    const p = s[sampleAt(s, m.xAt(((e.clientX - r.left) * CW) / r.width))];
    const x = m.X(p.d_km);
    cursor.setAttribute('x1', x);
    cursor.setAttribute('x2', x);
    cursor.setAttribute('visibility', 'visible');
    readout.textContent = `${fmtDistance(p.d_km)} · ${fmtElev(p.z_m)}${p.water ? ' · water' : ''}`;
    onHover(p);
  });
  chart.addEventListener('pointerleave', () => {
    cursor.setAttribute('visibility', 'hidden');
    readout.textContent = ' ';
    onHover(null);
  });
  const st = prof.stats;
  box.replaceChildren(chart, readout,
    el('p', {}, `Highest ${fmtElev(st.max_m)} · lowest ${fmtElev(st.min_m)}`, el('br'),
      `Climb ${int(st.ascent_m)} m · descent ${int(st.descent_m)} m · over water ${fmtDistance(st.water_km)}`));
}