// 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)}`));
}