worldmap-viewer

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

master

raw · 2834 bytes

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