mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-03 15:36:24 +08:00
Flight model (ground/air physics, SELECTED/NAV/APPR/AUTOLAND autopilot with STAR sequencing and ILS capture), bridge client that feeds the existing /api/bridge/* endpoints so /live-atc can't tell it apart from a real bridge, and the cockpit UI (PFD reuse, FCU, radio panel, Leaflet ND, three.js exterior, spawn presets at EDDF/EDDS). Design doc: docs/plans/2026-07-16-websim-design.md. Also adds a local-dev-only auto-login (/dev-login, server/api/dev/login.post.ts) that bypasses the invite-only login and MongoDB entirely via a fixed in-memory user, so require-auth pages are reachable for local testing even when the dev DB is unreachable. Hard-disabled outside development. Status: unit tests green (yarn test) and typecheck clean (yarn typecheck). Browser walkthrough of the actual cockpit (flying a preset, confirming telemetry reaches /live-atc) is not yet done — picking up from a fresh dev server + /dev-login?redirect=/flightlab/websim confirmed the spawn screen renders past auth, but full instrument/map/exterior verification is still outstanding. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
74 lines
2.4 KiB
TypeScript
74 lines
2.4 KiB
TypeScript
// Spherical-earth geo helpers for the WebSim flight model (position
|
|
// integration, STAR/ILS geometry) and for generating the hardcoded spawn
|
|
// preset coordinates from a runway threshold + bearing/distance instead of
|
|
// hand-typing derived lat/lons.
|
|
|
|
const EARTH_RADIUS_NM = 3440.065
|
|
|
|
function toRad(deg: number): number {
|
|
return (deg * Math.PI) / 180
|
|
}
|
|
|
|
function toDeg(rad: number): number {
|
|
return (rad * 180) / Math.PI
|
|
}
|
|
|
|
/** Wrap any degree value into [0, 360). */
|
|
export function normalizeHeading(deg: number): number {
|
|
return ((deg % 360) + 360) % 360
|
|
}
|
|
|
|
/** Smallest signed difference `to - from`, in (-180, 180]. */
|
|
export function angleDiffDeg(from: number, to: number): number {
|
|
return ((((to - from) % 360) + 540) % 360) - 180
|
|
}
|
|
|
|
/** Great-circle distance in nautical miles. */
|
|
export function distanceNm(lat1: number, lon1: number, lat2: number, lon2: number): number {
|
|
const phi1 = toRad(lat1)
|
|
const phi2 = toRad(lat2)
|
|
const dPhi = toRad(lat2 - lat1)
|
|
const dLambda = toRad(lon2 - lon1)
|
|
const a = Math.sin(dPhi / 2) ** 2 + Math.cos(phi1) * Math.cos(phi2) * Math.sin(dLambda / 2) ** 2
|
|
return EARTH_RADIUS_NM * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
|
}
|
|
|
|
/** Initial bearing (degrees true, 0-360) from point 1 to point 2. */
|
|
export function bearingDeg(lat1: number, lon1: number, lat2: number, lon2: number): number {
|
|
const phi1 = toRad(lat1)
|
|
const phi2 = toRad(lat2)
|
|
const dLambda = toRad(lon2 - lon1)
|
|
const y = Math.sin(dLambda) * Math.cos(phi2)
|
|
const x = Math.cos(phi1) * Math.sin(phi2) - Math.sin(phi1) * Math.cos(phi2) * Math.cos(dLambda)
|
|
return normalizeHeading(toDeg(Math.atan2(y, x)))
|
|
}
|
|
|
|
/** Point reached from (lat, lon) heading `bearing` degrees for `distanceNm` nautical miles. */
|
|
export function destinationPoint(
|
|
lat: number,
|
|
lon: number,
|
|
bearing: number,
|
|
distance: number,
|
|
): { lat: number; lon: number } {
|
|
const delta = distance / EARTH_RADIUS_NM
|
|
const theta = toRad(bearing)
|
|
const phi1 = toRad(lat)
|
|
const lambda1 = toRad(lon)
|
|
|
|
const phi2 = Math.asin(
|
|
Math.sin(phi1) * Math.cos(delta) + Math.cos(phi1) * Math.sin(delta) * Math.cos(theta),
|
|
)
|
|
const lambda2 =
|
|
lambda1 +
|
|
Math.atan2(
|
|
Math.sin(theta) * Math.sin(delta) * Math.cos(phi1),
|
|
Math.cos(delta) - Math.sin(phi1) * Math.sin(phi2),
|
|
)
|
|
|
|
return { lat: toDeg(phi2), lon: normalizeLon(toDeg(lambda2)) }
|
|
}
|
|
|
|
function normalizeLon(deg: number): number {
|
|
return ((deg + 540) % 360) - 180
|
|
}
|