DMRmap/frontend/src/geo.ts
2026-02-19 17:00:59 +01:00

89 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { LatLngTuple } from "./types";
const EARTH_RADIUS_KM = 6371;
const DEG_TO_RAD = Math.PI / 180;
const RAD_TO_DEG = 180 / Math.PI;
/** Convert lat/lng to 6-character Maidenhead grid locator. */
export function toMaidenhead(lat: number, lng: number): string {
lng = lng + 180;
lat = lat + 90;
let loc = "";
loc += String.fromCharCode(65 + Math.floor(lng / 20));
loc += String.fromCharCode(65 + Math.floor(lat / 10));
lng = lng % 20;
lat = lat % 10;
loc += Math.floor(lng / 2);
loc += Math.floor(lat);
lng = (lng % 2) * 60;
lat = (lat % 1) * 60;
loc += String.fromCharCode(97 + Math.floor(lng / 5));
loc += String.fromCharCode(97 + Math.floor(lat / 2.5));
return loc;
}
/** Move a point along a bearing by a distance (great-circle). */
export function offsetPoint(lat: number, lng: number, bearing: number, distKm: number): LatLngTuple {
const lat1 = lat * DEG_TO_RAD;
const lng1 = lng * DEG_TO_RAD;
const brng = bearing * DEG_TO_RAD;
const d = distKm / EARTH_RADIUS_KM;
const lat2 = Math.asin(Math.sin(lat1) * Math.cos(d) + Math.cos(lat1) * Math.sin(d) * Math.cos(brng));
const lng2 = lng1 + Math.atan2(Math.sin(brng) * Math.sin(d) * Math.cos(lat1),
Math.cos(d) - Math.sin(lat1) * Math.sin(lat2));
return [lat2 * RAD_TO_DEG, lng2 * RAD_TO_DEG];
}
/** Initial bearing (forward azimuth) from p1 to p2 in degrees [0, 360). */
export function getBearing(p1: LatLngTuple, p2: LatLngTuple): number {
const lat1 = p1[0] * DEG_TO_RAD;
const lat2 = p2[0] * DEG_TO_RAD;
const dLng = (p2[1] - p1[1]) * DEG_TO_RAD;
const y = Math.sin(dLng) * Math.cos(lat2);
const x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLng);
return (Math.atan2(y, x) * RAD_TO_DEG + 360) % 360;
}
/** Average of two bearings via unit-vector mean. */
export function averageBearing(b1: number, b2: number): number {
const r1 = b1 * DEG_TO_RAD;
const r2 = b2 * DEG_TO_RAD;
const x = Math.cos(r1) + Math.cos(r2);
const y = Math.sin(r1) + Math.sin(r2);
return (Math.atan2(y, x) * RAD_TO_DEG + 360) % 360;
}
/** Build a closed polygon outlining the corridor around a polyline. */
export function computeCorridorPolygon(points: LatLngTuple[], distKm: number): LatLngTuple[] {
if (points.length < 2) return [];
const left: LatLngTuple[] = [];
const right: LatLngTuple[] = [];
for (let i = 0; i < points.length; i++) {
let bearing: number;
if (i === 0) {
bearing = getBearing(points[0], points[1]);
} else if (i === points.length - 1) {
bearing = getBearing(points[i - 1], points[i]);
} else {
const b1 = getBearing(points[i - 1], points[i]);
const b2 = getBearing(points[i], points[i + 1]);
bearing = averageBearing(b1, b2);
}
left.push(offsetPoint(points[i][0], points[i][1], bearing - 90, distKm));
right.push(offsetPoint(points[i][0], points[i][1], bearing + 90, distKm));
}
return left.concat(right.reverse());
}
/** Map a 01 intensity value to an RGB color string (green→yellow→red). */
export function heatColor(intensity: number): string {
let r: number, g: number;
if (intensity < 0.5) {
r = Math.round(255 * (intensity * 2));
g = 255;
} else {
r = 255;
g = Math.round(255 * (1 - (intensity - 0.5) * 2));
}
return "rgb(" + r + "," + g + ",0)";
}