trails/apps/planner/app/lib/poi-snap.ts
Ullrich Schäfer 6d82ebe127
Store POI metadata (osmId, tags) on waypoints for Journal use
When a waypoint is created from or snapped to a POI, the Yjs Y.Map
now stores osmId (OSM node ID) and poiTags (phone, website, address,
opening hours, etc.). Dragging away clears this data.

This persists through the Yjs document and will be available when the
route is saved to the Journal, enabling future display of campsite
contact details, opening hours, etc. on route detail pages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 02:02:47 +02:00

66 lines
1.9 KiB
TypeScript

import type { Poi } from "./overpass.ts";
const SNAP_DISTANCE_METERS = 50;
export interface SnapResult {
lat: number;
lon: number;
name?: string;
snapped: boolean;
/** OSM node ID if snapped */
osmId?: number;
/** Key POI tags if snapped */
poiTags?: Record<string, string>;
}
/**
* Snap a coordinate to the nearest visible POI if within threshold.
* Returns the snapped position and POI name, or the original position.
*/
export function snapToPoi(lat: number, lon: number, pois: Poi[]): SnapResult {
if (pois.length === 0) return { lat, lon, snapped: false };
let bestPoi: Poi | null = null;
let bestDist = Infinity;
for (const poi of pois) {
const dist = haversine(lat, lon, poi.lat, poi.lon);
if (dist < bestDist) {
bestDist = dist;
bestPoi = poi;
}
}
if (bestPoi && bestDist <= SNAP_DISTANCE_METERS) {
// Pick key tags worth persisting with the waypoint
const keepTags = ["phone", "contact:phone", "website", "opening_hours",
"addr:street", "addr:housenumber", "addr:postcode", "addr:city",
"description", "amenity", "tourism", "shop"];
const poiTags: Record<string, string> = {};
for (const key of keepTags) {
if (bestPoi.tags[key]) poiTags[key] = bestPoi.tags[key];
}
return {
lat: bestPoi.lat,
lon: bestPoi.lon,
name: bestPoi.name,
snapped: true,
osmId: bestPoi.id,
poiTags: Object.keys(poiTags).length > 0 ? poiTags : undefined,
};
}
return { lat, lon, snapped: false };
}
function haversine(lat1: number, lon1: number, lat2: number, lon2: number): number {
const R = 6371000;
const toRad = (d: number) => (d * Math.PI) / 180;
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}