aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/viewer/js/search.js
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-07 00:14:38 +0200
committergodosa <godosa@godosa.eu>2026-10-07 00:14:38 +0200
commit3443c1c65e9f1753e1e656b35d08416c1fa298f2 (patch)
tree4e43236f460145a4d75d1b4616dcb7aa6ef08f51 /viewer/js/search.js
downloadworldmap-viewer-3443c1c65e9f1753e1e656b35d08416c1fa298f2.tar.gz
worldmap-viewer-3443c1c65e9f1753e1e656b35d08416c1fa298f2.zip
worldmap-viewer: initial public history
Diffstat (limited to 'viewer/js/search.js')
-rw-r--r--viewer/js/search.js68
1 files changed, 68 insertions, 0 deletions
diff --git a/viewer/js/search.js b/viewer/js/search.js
new file mode 100644
index 0000000..3ddc29c
--- /dev/null
+++ b/viewer/js/search.js
@@ -0,0 +1,68 @@
+// search.js — search box logic (pure): coordinates, H3 cell ids and pin matches → suggestions
+import { filterPins } from './pins.js';
+import { fmtLatLon } from './units.js';
+
+const HEMI = { N: ['lat', 1], S: ['lat', -1], E: ['lon', 1], W: ['lon', -1] };
+const MARKS = ['°', "'", '"'];
+
+function tokens(q) {
+ return q.toUpperCase()
+ .replace(/[°º]/g, ' ° ').replace(/['′’]/g, " ' ").replace(/["″”]/g, ' " ')
+ .replace(/([NSEW])/g, ' $1 ').replace(/[,;]/g, ' , ')
+ .split(/\s+/).filter(Boolean);
+}
+
+export function parseCoords(q) { // "lat,lon" first; failing that, commas between digits are decimal commas
+ return parseCoordsAs(q) ?? (/\d,\d/.test(q) ? parseCoordsAs(q.replace(/(\d),(\d)/g, '$1.$2')) : null);
+}
+
+function parseCoordsAs(q) {
+ const comps = [];
+ let cur = null;
+ const close = () => { if (cur?.parts.length) comps.push(cur); cur = null; };
+ const toks = tokens(q);
+ for (let i = 0; i < toks.length; i++) {
+ const t = toks[i];
+ if (t === ',') { close(); continue; }
+ if (HEMI[t]) {
+ if (cur?.parts.length && !cur.hemi) { cur.hemi = t; close(); }
+ else { close(); cur = { parts: [], units: [], hemi: t }; }
+ continue;
+ }
+ if (!/^[+-]?\d+(\.\d+)?$/.test(t)) return null;
+ const mark = MARKS.includes(toks[i + 1]) ? MARKS.indexOf(toks[++i]) : null;
+ if (cur?.parts.length && (mark === null || mark <= cur.units.at(-1))) close(); // bare number or restarting unit
+ cur ??= { parts: [], units: [], hemi: null };
+ cur.parts.push(parseFloat(t));
+ cur.units.push(mark ?? 0);
+ }
+ close();
+ if (comps.length !== 2 || comps.some(c => c.parts.slice(1).some(v => v < 0 || v >= 60))) return null;
+ const val = c => c.parts.reduce((a, v, k) => a + Math.abs(v) / 60 ** c.units[k], 0) * (c.parts[0] < 0 ? -1 : 1);
+ const out = {};
+ const lettered = comps.filter(c => c.hemi);
+ for (const c of lettered) {
+ const [axis, sign] = HEMI[c.hemi];
+ if (axis in out) return null;
+ out[axis] = Math.abs(val(c)) * sign;
+ }
+ const bare = comps.filter(c => !c.hemi);
+ for (const c of bare) out['lat' in out ? 'lon' : 'lat'] = val(c);
+ if (!(Math.abs(out.lat) <= 90) || !(Math.abs(out.lon) <= 180)) return null;
+ return { lat: out.lat, lon: out.lon };
+}
+
+export const isCellId = q => /^8[0-9a-f]{14}$/i.test(q.trim());
+
+export function suggest(q, pins, max = 8) {
+ const s = q.trim();
+ if (!s) return [];
+ const out = [];
+ const c = parseCoords(s);
+ if (c) out.push({ kind: 'coords', label: fmtLatLon(c.lat, c.lon), ...c });
+ if (isCellId(s)) out.push({ kind: 'cell', label: `H3 cell ${s.toLowerCase()}`, cell: s.toLowerCase() });
+ for (const p of filterPins(pins, s).slice(0, Math.max(0, max - out.length))) {
+ out.push({ kind: 'pin', label: p.name, sub: p.lore || fmtLatLon(p.lat, p.lon), lat: p.lat, lon: p.lon });
+ }
+ return out;
+}