// regions.js — refined-region outlines (pure): closing a drawn outline, self-crossing check, payloads, status text export function closesOutline(first, click, px = 12) { return !!first && Math.hypot(first.x - click.x, first.y - click.y) <= px; } // a click while drawing (screen points): 'close' on the first point, or on the last one (a double-click), once there // are 3 points; 'ignore' a repeat of the last point before that; else 'add' export function outlineClick(first, last, at, n, px = 12) { if (n >= 3 && (closesOutline(first, at, px) || closesOutline(last, at, px))) return 'close'; return closesOutline(last, at, px) ? 'ignore' : 'add'; } const unwrap = pts => pts.map(p => ({ lat: p.lat, lon: pts[0].lon + ((((p.lon - pts[0].lon + 180) % 360) + 360) % 360) - 180 })); export function crossesItself(points) { const p = unwrap(points), n = p.length; const ccw = (a, b, c) => (c.lat - a.lat) * (b.lon - a.lon) - (b.lat - a.lat) * (c.lon - a.lon); for (let i = 0; i < n; i++) { for (let j = i + 1; j < n; j++) { if (j === i + 1 || (i === 0 && j === n - 1)) continue; // neighbours share a point const a = p[i], b = p[(i + 1) % n], c = p[j], d = p[(j + 1) % n]; if (ccw(a, b, c) * ccw(a, b, d) < 0 && ccw(c, d, a) * ccw(c, d, b) < 0) return true; } } return false; } export const regionPayload = (name, points) => ({ name: name.trim(), outline: points.map(p => [p.lat, p.lon]) }); export function statusText(s) { const t = s?.tiles, saving = t && t.total > 0 && t.done < t.total ? Math.round((100 * t.done) / t.total) : null; if (!s || s.state === 'idle') return saving === null ? '' : `Saving region tiles ${saving}%`; if (s.state === 'queued') return 'Queued…'; if (s.state === 'building') return `Refining… ${Math.round((s.fraction ?? 0) * 100)}%${s.stage ? ` (${s.stage})` : ''}`; if (s.state === 'failed') return `Refinement failed: ${s.error ?? 'unknown error'}`; return `Refined areas are up to date${saving === null ? '' : ` · saving tiles ${saving}%`}`; } export const outlineLines = regions => regions.map(r => { const pts = r.outline.map(([lat, lon]) => ({ lat, lon })); return [...pts, pts[0]]; }); import { R_KM } from './geo.js'; export { R_KM }; export const WARN_KM2 = 2e6; // bigger regions take minutes and much memory: ask first export function outlineAreaKm2(points, radiusKm = R_KM) { // spherical polygon area (edges as lat/lon lines) const p = unwrap(points), r = Math.PI / 180; let sum = 0; for (let i = 0; i < p.length; i++) { const a = p[i], b = p[(i + 1) % p.length]; sum += (b.lon - a.lon) * r * (2 + Math.sin(a.lat * r) + Math.sin(b.lat * r)); } return Math.abs(sum) * radiusKm ** 2 / 2; } export const needsTileSwitch = (current, tiles) => !!tiles?.url && tiles.url !== current?.url;