mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
refactor: separate map feature queries and source records
This commit is contained in:
1 parent
a8125ad917
commit
bf1d50f29b
28 files changed
+1128
-675
No files matched your search
@@ -1,5 +1,3 @@
|
||||
import { normalizeMilitaryInstallations } from '../../data/militaryInstallationData.js';
|
||||
|
||||
export function createIngestion({
|
||||
state: layerState,
|
||||
services,
|
||||
@@ -58,19 +56,15 @@ export function createIngestion({
|
||||
// A SATURATED snapped tile was truncated upstream, so features from the
|
||||
// snap's extra ring may have crowded out sites actually on screen. Re-ask
|
||||
// for the exact viewport (separately keyed and cached) before rendering.
|
||||
let saturated = parts.model.installationResponseSaturated(payload);
|
||||
let saturated = payload.saturated === true;
|
||||
if (saturated) {
|
||||
payload = await fetchInstallations(true);
|
||||
saturated = parts.model.installationResponseSaturated(payload);
|
||||
saturated = payload.saturated === true;
|
||||
}
|
||||
const normalized = normalizeMilitaryInstallations(
|
||||
payload,
|
||||
payload.retrievedAt || new Date().toISOString(),
|
||||
);
|
||||
// The proxy answers a bbox at least as large as the viewport; keep only what
|
||||
// was actually asked for so nothing off-screen reaches the map or the
|
||||
// "current viewport only" context claim.
|
||||
const records = normalized.records.filter((record) =>
|
||||
const records = payload.records.filter((record) =>
|
||||
parts.model.installationWithinViewport(record, box),
|
||||
);
|
||||
let placesError = null;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { installationResponseSaturated } from './source.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import {
|
||||
EARTH_MEAN_RADIUS_M,
|
||||
@@ -137,23 +138,6 @@ export function createModel({ state: layerState, services, parts, source }) {
|
||||
return record.osmType !== 'node';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a response was truncated at the upstream element cap.
|
||||
*
|
||||
* The proxy states this outright, but a `saturated`-less payload is NOT
|
||||
* evidence of a complete answer: entries cached before the saturation guard
|
||||
* shipped predate the field and live for 30 days. Fall back to deriving it from
|
||||
* the element count against the cap the payload itself reports.
|
||||
* @param {{saturated?: boolean, elements?: Array, elementCap?: number}} payload
|
||||
* @returns {boolean}
|
||||
*/
|
||||
|
||||
function installationResponseSaturated(payload) {
|
||||
if (typeof payload?.saturated === 'boolean') return payload.saturated;
|
||||
const cap = Number(payload?.elementCap);
|
||||
if (!Number.isFinite(cap) || cap <= 0) return false;
|
||||
return Array.isArray(payload?.elements) && payload.elements.length >= cap;
|
||||
}
|
||||
return {
|
||||
approximateSurfaceDistanceM,
|
||||
colorFor,
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
import { normalizeMilitaryInstallations } from '../../data/militaryInstallationData.js';
|
||||
|
||||
/** Preserve legacy cache admission even when the explicit saturation flag is absent. */
|
||||
export function installationResponseSaturated(payload) {
|
||||
if (typeof payload?.saturated === 'boolean') return payload.saturated;
|
||||
const cap = Number(payload?.elementCap);
|
||||
if (!Number.isFinite(cap) || cap <= 0) return false;
|
||||
return Array.isArray(payload?.elements) && payload.elements.length >= cap;
|
||||
}
|
||||
|
||||
/** Read mapped installations and explicit nearby-place searches through fixed endpoints. */
|
||||
export function createInstallationSource({
|
||||
fetchImpl = (...args) => globalThis.fetch(...args),
|
||||
@@ -43,7 +53,14 @@ export function createInstallationSource({
|
||||
);
|
||||
if (!Array.isArray(body?.elements))
|
||||
throw new Error('Malformed installation snapshot');
|
||||
return body;
|
||||
return {
|
||||
...normalizeMilitaryInstallations(
|
||||
body,
|
||||
body.retrievedAt || new Date().toISOString(),
|
||||
),
|
||||
status: body.status,
|
||||
saturated: installationResponseSaturated(body),
|
||||
};
|
||||
},
|
||||
async searchNearby({ latitude, longitude, radiusM }, { signal } = {}) {
|
||||
if (
|
||||
|
||||
@@ -57,3 +57,30 @@ test('installation response parsing respects cancellation before any follow-on s
|
||||
{ name: 'AbortError' },
|
||||
);
|
||||
});
|
||||
|
||||
test('mapped-site sources normalize records and derive saturation from legacy cache metadata', async () => {
|
||||
const source = createInstallationSource({
|
||||
fetchImpl: async () =>
|
||||
Response.json({
|
||||
elements: [
|
||||
{
|
||||
type: 'node',
|
||||
id: 1,
|
||||
lat: 30.2,
|
||||
lon: -97.7,
|
||||
tags: { military: 'airfield', name: 'Fixture Field' },
|
||||
},
|
||||
],
|
||||
elementCap: 1,
|
||||
status: 'stale',
|
||||
retrievedAt: '2026-01-01T00:00:00Z',
|
||||
}),
|
||||
});
|
||||
const payload = await source.getMappedSites(box);
|
||||
assert.equal(payload.records[0].name, 'Fixture Field');
|
||||
assert.equal(payload.records[0].latitude, 30.2);
|
||||
assert.equal(payload.records[0].retrievedAt, '2026-01-01T00:00:00Z');
|
||||
assert.equal(payload.saturated, true);
|
||||
assert.equal(payload.status, 'stale');
|
||||
assert.equal('elements' in payload, false);
|
||||
});
|
||||
@@ -73,7 +73,7 @@ export function createIngestion({
|
||||
if (!state) {
|
||||
const data = await response.json();
|
||||
signal?.throwIfAborted();
|
||||
if (!Array.isArray(data?.elements))
|
||||
if (!Array.isArray(data?.roads))
|
||||
throw new Error('Malformed road snapshot');
|
||||
return data;
|
||||
}
|
||||
@@ -100,8 +100,7 @@ export function createIngestion({
|
||||
}
|
||||
const data = await response.json();
|
||||
signal?.throwIfAborted();
|
||||
if (!Array.isArray(data?.elements))
|
||||
throw new Error('Malformed road snapshot');
|
||||
if (!Array.isArray(data?.roads)) throw new Error('Malformed road snapshot');
|
||||
if (state) {
|
||||
const responseEnd = parts.timing.trafficTimingMark(
|
||||
state,
|
||||
|
||||
+10
-58
@@ -13,52 +13,17 @@ import {
|
||||
} from './policy.js';
|
||||
|
||||
export function createModel({ state: layerState, services, parts, source }) {
|
||||
// ─── Overpass API ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Build an Overpass QL query string to fetch road ways within a bounding box.
|
||||
*
|
||||
* The query uses a highway tag regex filter. When `majorOnly` is true, only
|
||||
* motorway/trunk/primary/secondary are included; otherwise all drivable road
|
||||
* classes are fetched. The `out geom qt;` suffix returns inline geometry
|
||||
* (lat/lon per node) sorted by quadtile for faster server response.
|
||||
*
|
||||
* @param {number} south - Southern latitude bound (degrees).
|
||||
* @param {number} west - Western longitude bound (degrees).
|
||||
* @param {number} north - Northern latitude bound (degrees).
|
||||
* @param {number} east - Eastern longitude bound (degrees).
|
||||
* @param {Object} [opts]
|
||||
* @param {boolean} [opts.majorOnly=false] - Restrict to major highway classes only.
|
||||
* @param {number} [opts.timeoutSec=25] - Overpass server-side timeout.
|
||||
* @returns {string} Overpass QL query body.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse an Overpass `out geom;` JSON response into internal road objects.
|
||||
*
|
||||
* Each OSM `way` element carries an inline `geometry` array of `{lat, lon}`
|
||||
* objects, so no separate node look-up or osmtogeojson conversion is needed.
|
||||
*
|
||||
* Processing per way:
|
||||
* 1. Extract [lon, lat] coordinate pairs.
|
||||
* 2. Sub-sample long polylines to at most MAX_WAYPOINTS_PER_ROAD vertices.
|
||||
* 3. Sample terrain height once at the first vertex (avoids per-vertex cost).
|
||||
* 4. Convert to Cartesian3 waypoints and pre-compute inter-vertex distances.
|
||||
*
|
||||
* @param {Object} overpassData - Raw JSON response from the Overpass API.
|
||||
* @param {Array} overpassData.elements - Array of OSM elements.
|
||||
* @returns {Array<{coords:number[][], type:string, waypoints:Cesium.Cartesian3[], segmentDist:number[]}>}
|
||||
* Parsed road objects ready for dot spawning.
|
||||
*/
|
||||
|
||||
function parseRoads(overpassData) {
|
||||
if (!overpassData || !overpassData.elements) return [];
|
||||
/** Build scene waypoints from source records; thinning and terrain remain rendering policy. */
|
||||
function parseRoads(roadData) {
|
||||
if (!roadData || !roadData.roads) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const roads = [];
|
||||
for (const el of overpassData.elements) {
|
||||
if (el.type !== 'way' || !el.geometry || el.geometry.length < 2) continue;
|
||||
for (const road of roadData.roads) {
|
||||
if (!road.coordinates || road.coordinates.length < 2) continue;
|
||||
|
||||
const rawCoords = el.geometry.map((g) => [g.lon, g.lat]);
|
||||
const rawCoords = road.coordinates;
|
||||
|
||||
// Sub-sample long polylines: keep every Nth vertex to stay within budget
|
||||
const simplifyStep =
|
||||
@@ -79,21 +44,8 @@ export function createModel({ state: layerState, services, parts, source }) {
|
||||
|
||||
if (coords.length < 2) continue;
|
||||
|
||||
const type = el.tags?.highway || 'unclassified';
|
||||
// One-way capture (field-test round 1: "a car would never go in
|
||||
// reverse"): dots on one-way roads all travel the legal direction.
|
||||
// OSM: oneway=yes/1/true → digitization order; '-1' → reversed;
|
||||
// roundabouts are one-way by definition. 0 = two-way (alternate).
|
||||
const onewayTag = el.tags?.oneway;
|
||||
const oneway =
|
||||
onewayTag === 'yes' ||
|
||||
onewayTag === '1' ||
|
||||
onewayTag === 'true' ||
|
||||
el.tags?.junction === 'roundabout'
|
||||
? 1
|
||||
: onewayTag === '-1'
|
||||
? -1
|
||||
: 0;
|
||||
const type = road.type;
|
||||
const oneway = road.oneway;
|
||||
|
||||
// Sample terrain height once at the road start to avoid per-vertex cost
|
||||
let baseHeight = 0;
|
||||
|
||||
@@ -82,13 +82,13 @@ function roads(bounds) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
elements: [
|
||||
roads: [
|
||||
{
|
||||
type: 'way',
|
||||
tags: { highway: 'primary' },
|
||||
geometry: [
|
||||
{ lat: bounds.south, lon: bounds.west },
|
||||
{ lat: bounds.south + 0.005, lon: bounds.west + 0.005 },
|
||||
type: 'primary',
|
||||
oneway: 0,
|
||||
coordinates: [
|
||||
[bounds.west, bounds.south],
|
||||
[bounds.west + 0.005, bounds.south + 0.005],
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { normalizeOverpassRoads } from '../../sources/overpassRoads.js';
|
||||
export { normalizeOverpassRoads } from '../../sources/overpassRoads.js';
|
||||
import { createFlowTileSource } from './flowSource.js';
|
||||
function buildOverpassQuery(
|
||||
south,
|
||||
@@ -58,7 +60,9 @@ export function createTrafficSource({
|
||||
async json() {
|
||||
const body = await response.json();
|
||||
signal?.throwIfAborted();
|
||||
return body;
|
||||
if (!Array.isArray(body?.elements))
|
||||
throw new Error('Malformed road snapshot');
|
||||
return { roads: normalizeOverpassRoads(body) };
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -127,3 +127,41 @@ test('road body parsing retains the source request cancellation signal', async (
|
||||
});
|
||||
await assert.rejects(response.json(), { name: 'AbortError' });
|
||||
});
|
||||
|
||||
test('road sources decode direction and coordinates before scene construction', async () => {
|
||||
const geometry = [
|
||||
{ lat: 30, lon: -97 },
|
||||
{ lat: 30.001, lon: -97.001 },
|
||||
];
|
||||
const source = createTrafficSource({
|
||||
fetchImpl: async () =>
|
||||
Response.json({
|
||||
elements: [
|
||||
{ type: 'node', id: 1 },
|
||||
{ type: 'way', geometry, tags: { highway: 'primary', oneway: '-1' } },
|
||||
{ type: 'way', geometry, tags: { junction: 'roundabout' } },
|
||||
{ type: 'way', geometry: [geometry[0]] },
|
||||
],
|
||||
}),
|
||||
});
|
||||
assert.deepEqual(await (await source.requestRoads(bounds)).json(), {
|
||||
roads: [
|
||||
{
|
||||
coordinates: [
|
||||
[-97, 30],
|
||||
[-97.001, 30.001],
|
||||
],
|
||||
type: 'primary',
|
||||
oneway: -1,
|
||||
},
|
||||
{
|
||||
coordinates: [
|
||||
[-97, 30],
|
||||
[-97.001, 30.001],
|
||||
],
|
||||
type: 'unclassified',
|
||||
oneway: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
@@ -193,7 +193,7 @@ export function createTiming({ state: layerState, services, parts, source }) {
|
||||
* waypoint-materialization time independently.
|
||||
*/
|
||||
|
||||
function parseRoadsTimed(overpassData, trace) {
|
||||
function parseRoadsTimed(roadData, trace) {
|
||||
/* TRACE_ONLY_BEGIN */
|
||||
const _trafficTimingState = trafficTimingPass(
|
||||
trace,
|
||||
@@ -207,7 +207,7 @@ export function createTiming({ state: layerState, services, parts, source }) {
|
||||
_trafficTimingParseStartTime,
|
||||
);
|
||||
/* TRACE_ONLY_END */
|
||||
if (!overpassData || !overpassData.elements) {
|
||||
if (!roadData || !roadData.roads) {
|
||||
/* TRACE_ONLY_BEGIN */
|
||||
const _trafficTimingParseEnd = trafficTimingMark(
|
||||
_trafficTimingState,
|
||||
@@ -251,10 +251,10 @@ export function createTiming({ state: layerState, services, parts, source }) {
|
||||
let _trafficTimingSampleHeightMs = 0;
|
||||
let _trafficTimingWaypointMaterializationMs = 0;
|
||||
/* TRACE_ONLY_END */
|
||||
for (const el of overpassData.elements) {
|
||||
if (el.type !== 'way' || !el.geometry || el.geometry.length < 2) continue;
|
||||
for (const road of roadData.roads) {
|
||||
if (!road.coordinates || road.coordinates.length < 2) continue;
|
||||
|
||||
const rawCoords = el.geometry.map((g) => [g.lon, g.lat]);
|
||||
const rawCoords = road.coordinates;
|
||||
const simplifyStep =
|
||||
rawCoords.length > MAX_WAYPOINTS_PER_ROAD
|
||||
? Math.ceil(rawCoords.length / MAX_WAYPOINTS_PER_ROAD)
|
||||
@@ -271,17 +271,8 @@ export function createTiming({ state: layerState, services, parts, source }) {
|
||||
}
|
||||
if (coords.length < 2) continue;
|
||||
|
||||
const type = el.tags?.highway || 'unclassified';
|
||||
const onewayTag = el.tags?.oneway;
|
||||
const oneway =
|
||||
onewayTag === 'yes' ||
|
||||
onewayTag === '1' ||
|
||||
onewayTag === 'true' ||
|
||||
el.tags?.junction === 'roundabout'
|
||||
? 1
|
||||
: onewayTag === '-1'
|
||||
? -1
|
||||
: 0;
|
||||
const type = road.type;
|
||||
const oneway = road.oneway;
|
||||
|
||||
let baseHeight = 0;
|
||||
const firstCoord = coords[0];
|
||||
|
||||
Reference in new issue
Block a user