worldmap-viewer

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

master

raw · 2826 bytes

// 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;