import type { GeoPoint, Point2D } from "./types"; const DEG_TO_RAD = Math.PI / 180; const METERS_PER_DEG = 111_320; // at equator export interface Projection { toSVG(geo: GeoPoint): Point2D; toGeo(pt: Point2D): GeoPoint; viewBox: string; } /** * Create an equirectangular projection centered on the centroid of all points. * Scales so the bounding box fits ~800 SVG units wide with padding. */ export function createProjection(allPoints: GeoPoint[], targetWidth = 800, padding = 40): Projection { if (allPoints.length === 0) { return { toSVG: () => ({ x: 0, y: 0 }), toGeo: () => ({ lng: 0, lat: 0 }), viewBox: "0 0 800 600" }; } // Centroid const refLat = allPoints.reduce((s, p) => s + p.lat, 0) / allPoints.length; const refLng = allPoints.reduce((s, p) => s + p.lng, 0) / allPoints.length; const cosLat = Math.cos(refLat * DEG_TO_RAD); // Project all points to find bounds (meters from centroid) const projected = allPoints.map((p) => ({ x: (p.lng - refLng) * cosLat * METERS_PER_DEG, y: -(p.lat - refLat) * METERS_PER_DEG, // SVG y-down })); let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; for (const p of projected) { if (p.x < minX) minX = p.x; if (p.x > maxX) maxX = p.x; if (p.y < minY) minY = p.y; if (p.y > maxY) maxY = p.y; } const rangeX = maxX - minX || 1; const rangeY = maxY - minY || 1; const scale = (targetWidth - 2 * padding) / Math.max(rangeX, rangeY); const cx = (minX + maxX) / 2; const cy = (minY + maxY) / 2; const svgW = rangeX * scale + 2 * padding; const svgH = rangeY * scale + 2 * padding; function toSVG(geo: GeoPoint): Point2D { const mx = (geo.lng - refLng) * cosLat * METERS_PER_DEG; const my = -(geo.lat - refLat) * METERS_PER_DEG; return { x: (mx - cx) * scale + svgW / 2, y: (my - cy) * scale + svgH / 2, }; } function toGeo(pt: Point2D): GeoPoint { const mx = (pt.x - svgW / 2) / scale + cx; const my = (pt.y - svgH / 2) / scale + cy; return { lng: mx / (cosLat * METERS_PER_DEG) + refLng, lat: -(my / METERS_PER_DEG) + refLat, }; } return { toSVG, toGeo, viewBox: `0 0 ${Math.round(svgW)} ${Math.round(svgH)}`, }; }