feat(map): add synthetic moving target layer
This commit is contained in:
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user