Release God's Eye View as open source

This commit is contained in:
Bilawal Sidhu committed 2026-08-24 17:02:16 -05:00
1 parent a5aaea7e16
commit 880a672b5e
433 files changed
+219893 -23

No files matched your search

+138
View File
@@ -0,0 +1,138 @@
/**
* @file Pure geometry helpers for the traffic layer's viewport fetch bounds.
*
* C4 fix (2026-07-16): `camera.computeViewRectangle()` spans toward the horizon
* at oblique pitch, and centering the fetch box on the RECTANGLE MIDPOINT put
* road fetches tens of km from what the user is actually looking at. The fetch
* center is now derived from the camera's look-at ground point
* (`camera.pickEllipsoid` at canvas center — the globe is hidden under Google
* 3D tiles, so `scene.globe.pick` is not reliable), falling back to the camera
* nadir, and pulled back toward nadir when the look-at point is beyond a
* horizon-gaze cap. Kept Cesium-free so it can be unit-tested with node:test.
*
* @module data/trafficBounds
*/
/** @const {number} Mean Earth radius in km (spherical approximation). */
const EARTH_RADIUS_KM = 6371;
const toRad = (deg) => (deg * Math.PI) / 180;
const toDeg = (rad) => (rad * 180) / Math.PI;
/**
* Great-circle distance between two lat/lon points (haversine).
*
* @param {number} lat1 - First point latitude (degrees).
* @param {number} lon1 - First point longitude (degrees).
* @param {number} lat2 - Second point latitude (degrees).
* @param {number} lon2 - Second point longitude (degrees).
* @returns {number} Distance in kilometres.
*/
export function greatCircleKm(lat1, lon1, lat2, lon2) {
const p1 = toRad(lat1);
const p2 = toRad(lat2);
const dp = toRad(lat2 - lat1);
const dl = toRad(lon2 - lon1);
const a = Math.sin(dp / 2) ** 2
+ Math.cos(p1) * Math.cos(p2) * Math.sin(dl / 2) ** 2;
return 2 * EARTH_RADIUS_KM * Math.asin(Math.min(1, Math.sqrt(a)));
}
/**
* Initial bearing (radians) from point 1 toward point 2 along the great circle.
*
* @param {number} lat1 @param {number} lon1 @param {number} lat2 @param {number} lon2
* @returns {number} Bearing in radians (0 = north, clockwise).
*/
function initialBearingRad(lat1, lon1, lat2, lon2) {
const p1 = toRad(lat1);
const p2 = toRad(lat2);
const dl = toRad(lon2 - lon1);
const y = Math.sin(dl) * Math.cos(p2);
const x = Math.cos(p1) * Math.sin(p2) - Math.sin(p1) * Math.cos(p2) * Math.cos(dl);
return Math.atan2(y, x);
}
/**
* Destination point given a start, an initial bearing, and a distance
* (spherical direct geodesic).
*
* @param {number} lat - Start latitude (degrees).
* @param {number} lon - Start longitude (degrees).
* @param {number} bearingRad - Initial bearing (radians, 0 = north).
* @param {number} distKm - Distance to travel (km).
* @returns {{lat:number, lon:number}} Destination in degrees.
*/
function destinationPoint(lat, lon, bearingRad, distKm) {
const delta = distKm / EARTH_RADIUS_KM;
const p1 = toRad(lat);
const l1 = toRad(lon);
const p2 = Math.asin(
Math.sin(p1) * Math.cos(delta) + Math.cos(p1) * Math.sin(delta) * Math.cos(bearingRad)
);
const l2 = l1 + Math.atan2(
Math.sin(bearingRad) * Math.sin(delta) * Math.cos(p1),
Math.cos(delta) - Math.sin(p1) * Math.sin(p2)
);
// Normalize longitude to [-180, 180)
const lonDeg = ((toDeg(l2) + 540) % 360) - 180;
return { lat: toDeg(p2), lon: lonDeg };
}
/**
* Derive the road-fetch center from the camera's look-at ground point.
*
* Rules:
* - No usable hit (pickEllipsoid returned undefined → non-finite hit coords):
* fall back to the camera nadir.
* - Hit within `maxPullKm` of nadir: use the hit verbatim (normal oblique view).
* - Hit farther than `maxPullKm` (horizon gaze): pull it back toward nadir to
* exactly `maxPullKm` along the nadir→hit great-circle bearing. Traffic only
* activates below 8 km camera altitude, so a look-at point farther than
* ~12 km is horizon-gazing, not something the user can see roads at.
*
* @param {Object} args
* @param {number} args.nadirLat - Camera nadir latitude (degrees).
* @param {number} args.nadirLon - Camera nadir longitude (degrees).
* @param {number} [args.hitLat] - pickEllipsoid ground-hit latitude (degrees), if any.
* @param {number} [args.hitLon] - pickEllipsoid ground-hit longitude (degrees), if any.
* @param {number} [args.maxPullKm=12] - Max great-circle distance from nadir.
* @returns {{lat:number, lon:number, source:'hit'|'nadir'|'pulled'}}
* The fetch center and which rule produced it.
*/
export function deriveFetchCenter({ nadirLat, nadirLon, hitLat, hitLon, maxPullKm = 12 }) {
if (!Number.isFinite(hitLat) || !Number.isFinite(hitLon)) {
return { lat: nadirLat, lon: nadirLon, source: 'nadir' };
}
const distKm = greatCircleKm(nadirLat, nadirLon, hitLat, hitLon);
if (distKm <= maxPullKm) {
return { lat: hitLat, lon: hitLon, source: 'hit' };
}
const bearing = initialBearingRad(nadirLat, nadirLon, hitLat, hitLon);
const pulled = destinationPoint(nadirLat, nadirLon, bearing, maxPullKm);
return { lat: pulled.lat, lon: pulled.lon, source: 'pulled' };
}
/**
* Clamp a bounding box's spans to `maxSpanDeg` and recenter it on `center`.
*
* Preserves the pre-C4 span semantics (each axis capped at 0.05° ≈ 5.5 km)
* but centers the box on the derived look-at point instead of the view
* rectangle's midpoint. Idempotent when `center` is the box's own midpoint.
*
* @param {{south:number, west:number, north:number, east:number}} bounds
* Source bounds (span donor).
* @param {{lat:number, lon:number}} center - Fetch center (degrees).
* @param {number} [maxSpanDeg=0.05] - Max span per axis in degrees.
* @returns {{south:number, west:number, north:number, east:number}} Clamped bounds.
*/
export function clampBoundsAroundCenter(bounds, center, maxSpanDeg = 0.05) {
const latSpan = Math.min(bounds.north - bounds.south, maxSpanDeg);
const lonSpan = Math.min(bounds.east - bounds.west, maxSpanDeg);
return {
south: center.lat - latSpan / 2,
north: center.lat + latSpan / 2,
west: center.lon - lonSpan / 2,
east: center.lon + lonSpan / 2,
};
}