diff options
| author | godosa <godosa@godosa.eu> | 2026-10-07 00:14:38 +0200 |
|---|---|---|
| committer | godosa <godosa@godosa.eu> | 2026-10-07 00:14:38 +0200 |
| commit | 3443c1c65e9f1753e1e656b35d08416c1fa298f2 (patch) | |
| tree | 4e43236f460145a4d75d1b4616dcb7aa6ef08f51 /viewer/js/search.js | |
| download | worldmap-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.js | 68 |
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; +} |
