aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/viewer/js/regions.js
diff options
context:
space:
mode:
Diffstat (limited to 'viewer/js/regions.js')
-rw-r--r--viewer/js/regions.js58
1 files changed, 58 insertions, 0 deletions
diff --git a/viewer/js/regions.js b/viewer/js/regions.js
new file mode 100644
index 0000000..de45385
--- /dev/null
+++ b/viewer/js/regions.js
@@ -0,0 +1,58 @@
+// 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;