refactor: separate map feature queries and source records

This commit is contained in:
Sameh Khamis committed 2026-09-15 08:10:32 -07:00
1 parent a8125ad917
commit bf1d50f29b
28 files changed
+1128 -675

No files matched your search

+2 -3
View File
@@ -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
View File
@@ -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;
+6 -6
View File
@@ -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],
],
},
],
+5 -1
View File
@@ -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) };
},
};
},
+38
View File
@@ -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,
},
],
});
});
+7 -16
View File
@@ -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];