feat(map): add synthetic moving target layer

This commit is contained in:
Codex
2026-07-29 18:28:54 +03:00
parent 65644a8609
commit 08a3c47391
6 changed files with 578 additions and 14 deletions
+112
View File
@@ -0,0 +1,112 @@
import type { MapGeoPoint, MapGhostPin } from "./contracts.js";
const EARTH_RADIUS_METERS = 6_371_008.8;
export function advanceGhostPins(
pins: readonly MapGhostPin[],
anchor: MapGeoPoint | null,
radiusMeters: number,
elapsedSeconds: number,
): MapGhostPin[] {
const safeElapsed = clamp(elapsedSeconds, 0, 1);
const safeRadius = Math.max(1, radiusMeters);
if (safeElapsed === 0) {
return pins.map((pin) => ({ ...pin }));
}
return pins.map((pin) => {
let heading = normalizeHeading(pin.heading_degrees);
if (anchor) {
const distanceFromAnchor = geodesicDistanceMeters(anchor, pin);
const projectedDistance =
distanceFromAnchor + Math.max(0, pin.speed_meters_per_second) * safeElapsed;
if (projectedDistance >= safeRadius * 0.98) {
heading = initialBearingDegrees(pin, anchor);
}
}
const position = destinationPoint(
pin,
heading,
Math.max(0, pin.speed_meters_per_second) * safeElapsed,
);
return {
...pin,
...position,
heading_degrees: heading,
};
});
}
export function destinationPoint(
origin: MapGeoPoint,
headingDegrees: number,
distanceMeters: number,
): MapGeoPoint {
const angularDistance = Math.max(0, distanceMeters) / EARTH_RADIUS_METERS;
const bearing = toRadians(normalizeHeading(headingDegrees));
const latitude = toRadians(origin.latitude);
const longitude = toRadians(origin.longitude);
const nextLatitude = Math.asin(
Math.sin(latitude) * Math.cos(angularDistance)
+ Math.cos(latitude) * Math.sin(angularDistance) * Math.cos(bearing),
);
const nextLongitude = longitude + Math.atan2(
Math.sin(bearing) * Math.sin(angularDistance) * Math.cos(latitude),
Math.cos(angularDistance) - Math.sin(latitude) * Math.sin(nextLatitude),
);
return {
longitude: normalizeLongitude(toDegrees(nextLongitude)),
latitude: clamp(toDegrees(nextLatitude), -90, 90),
};
}
export function geodesicDistanceMeters(
first: MapGeoPoint,
second: MapGeoPoint,
): number {
const latitudeDelta = toRadians(second.latitude - first.latitude);
const longitudeDelta = toRadians(second.longitude - first.longitude);
const firstLatitude = toRadians(first.latitude);
const secondLatitude = toRadians(second.latitude);
const haversine =
Math.sin(latitudeDelta / 2) ** 2
+ Math.cos(firstLatitude)
* Math.cos(secondLatitude)
* Math.sin(longitudeDelta / 2) ** 2;
return 2 * EARTH_RADIUS_METERS * Math.asin(Math.min(1, Math.sqrt(haversine)));
}
function initialBearingDegrees(
origin: MapGeoPoint,
destination: MapGeoPoint,
): number {
const firstLatitude = toRadians(origin.latitude);
const secondLatitude = toRadians(destination.latitude);
const longitudeDelta = toRadians(destination.longitude - origin.longitude);
const y = Math.sin(longitudeDelta) * Math.cos(secondLatitude);
const x =
Math.cos(firstLatitude) * Math.sin(secondLatitude)
- Math.sin(firstLatitude)
* Math.cos(secondLatitude)
* Math.cos(longitudeDelta);
return normalizeHeading(toDegrees(Math.atan2(y, x)));
}
function normalizeHeading(value: number): number {
return ((value % 360) + 360) % 360;
}
function normalizeLongitude(value: number): number {
return ((value + 540) % 360) - 180;
}
function toRadians(value: number): number {
return value * Math.PI / 180;
}
function toDegrees(value: number): number {
return value * 180 / Math.PI;
}
function clamp(value: number, minimum: number, maximum: number): number {
return Math.min(maximum, Math.max(minimum, value));
}