mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
Traffic roads come from OpenFreeMap vector tiles, or TomTom flow tiles with a key (Hybrid by default). Military areas use OpenFreeMap polygons with a bundled worldwide name index; wide views show named points. ALPR reads an hourly OpenStreetMap extract. Cockpit place context and country, state and US county outlines use bundled Natural Earth and US Census data. Overpass is used only when an operator configures their own endpoint. Also: camera arrivals no longer land below ground, keyless terrain retries on 429, bounded relation geometry for configured Overpass, one shared OpenStreetMap credit, and separate tile and API transports for embedders. Co-authored-by: theGaryLarson <[email protected]> Co-authored-by: Milan Khanal <[email protected]>
187 lines
8.4 KiB
JavaScript
187 lines
8.4 KiB
JavaScript
// src/data/flowMatch.test.mjs
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { matchFlowToRoads, median } from './flowMatch.js';
|
|
|
|
// Synthetic geometry around downtown Austin (cos-lat correction matters here).
|
|
const LAT0 = 30.26;
|
|
const LON0 = -97.7431;
|
|
const M_PER_DEG_LAT = 111320;
|
|
const M_PER_DEG_LON = M_PER_DEG_LAT * Math.cos((LAT0 * Math.PI) / 180);
|
|
|
|
/** Build a straight polyline from a start point, heading, length, and vertex spacing (meters). */
|
|
function line({ lat = LAT0, lon = LON0, northM = 0, eastM = 0, lengthM, stepM = 25, bearingDeg = 0 }) {
|
|
const startLat = lat + northM / M_PER_DEG_LAT;
|
|
const startLon = lon + eastM / M_PER_DEG_LON;
|
|
const steps = Math.max(1, Math.round(lengthM / stepM));
|
|
const coords = [];
|
|
const rad = (bearingDeg * Math.PI) / 180;
|
|
for (let i = 0; i <= steps; i++) {
|
|
const d = (lengthM * i) / steps;
|
|
coords.push([
|
|
startLon + (Math.sin(rad) * d) / M_PER_DEG_LON,
|
|
startLat + (Math.cos(rad) * d) / M_PER_DEG_LAT,
|
|
]);
|
|
}
|
|
return coords;
|
|
}
|
|
|
|
const ROAD_NS = { coords: line({ lengthM: 500, bearingDeg: 0 }), type: 'primary' };
|
|
|
|
function flow(coords, trafficLevel, { closure = false, roadType = 'Major road' } = {}) {
|
|
return { coords, trafficLevel, roadType, closure };
|
|
}
|
|
|
|
// ── matching core ───────────────────────────────────────────
|
|
|
|
test('coincident parallel road matches with the flow level', () => {
|
|
const flows = [flow(line({ lengthM: 500, bearingDeg: 0, eastM: 4 }), 0.42)];
|
|
const { matches, matchedCount, candidateCount } = matchFlowToRoads([ROAD_NS], flows);
|
|
assert.equal(matchedCount, 1);
|
|
assert.equal(candidateCount, 1);
|
|
assert.ok(matches[0], 'road should match');
|
|
assert.ok(Math.abs(matches[0].level - 0.42) < 1e-9);
|
|
assert.equal(matches[0].closure, false);
|
|
});
|
|
|
|
test('opposite-direction flow matches only the reverse travel direction)', () => {
|
|
// Same line drawn south->north vs road north->south equivalence.
|
|
const reversed = line({ lengthM: 500, bearingDeg: 0, eastM: 4 }).reverse();
|
|
const { matches } = matchFlowToRoads([{...ROAD_NS, oneway:-1}], [flow(reversed, 0.3)]);
|
|
assert.ok(matches[0], 'reversed flow should still match');
|
|
assert.ok(Math.abs(matches[0].level - 0.3) < 1e-9);
|
|
});
|
|
|
|
test('perpendicular decoy at the same location does NOT match', () => {
|
|
// East-west flow crossing the road's midpoint.
|
|
const decoy = flow(line({ northM: 250, eastM: -250, lengthM: 500, bearingDeg: 90 }), 0.1);
|
|
const { matches, matchedCount, candidateCount } = matchFlowToRoads([ROAD_NS], [decoy]);
|
|
assert.equal(matches[0], null);
|
|
assert.equal(matchedCount, 0);
|
|
// The decoy IS within 35 m of at least one sample — it's a candidate, just
|
|
// bearing-rejected. Coverage denominator must reflect that.
|
|
assert.equal(candidateCount, 1);
|
|
});
|
|
|
|
test('parallel flow offset by more than 35 m does not match', () => {
|
|
const farFlow = flow(line({ lengthM: 500, bearingDeg: 0, eastM: 50 }), 0.2);
|
|
const { matches, matchedCount, candidateCount } = matchFlowToRoads([ROAD_NS], [farFlow]);
|
|
assert.equal(matches[0], null);
|
|
assert.equal(matchedCount, 0);
|
|
assert.equal(candidateCount, 0);
|
|
});
|
|
|
|
test('closure propagates to the matched road', () => {
|
|
const closed = flow(line({ lengthM: 500, bearingDeg: 0, eastM: 2 }), 0, { closure: true });
|
|
const { matches } = matchFlowToRoads([ROAD_NS], [closed]);
|
|
assert.ok(matches[0], 'closed road should still match');
|
|
assert.equal(matches[0].closure, true);
|
|
});
|
|
|
|
test('closure propagates when only PART of the road is closed', () => {
|
|
const openHalf = flow(line({ lengthM: 250, bearingDeg: 0, eastM: 2 }), 0.9);
|
|
const closedHalf = flow(line({ northM: 250, lengthM: 250, bearingDeg: 0, eastM: 2 }), 0, { closure: true });
|
|
const { matches } = matchFlowToRoads([ROAD_NS], [openHalf, closedHalf]);
|
|
assert.ok(matches[0]);
|
|
assert.equal(matches[0].closure, true);
|
|
});
|
|
|
|
test('level is the median across matched samples (thirds at 0.2/0.4/0.9 -> 0.4)', () => {
|
|
const thirds = [
|
|
flow(line({ lengthM: 168, bearingDeg: 0, eastM: 2 }), 0.2),
|
|
flow(line({ northM: 168, lengthM: 166, bearingDeg: 0, eastM: 2 }), 0.4),
|
|
flow(line({ northM: 334, lengthM: 166, bearingDeg: 0, eastM: 2 }), 0.9),
|
|
];
|
|
const { matches } = matchFlowToRoads([ROAD_NS], thirds);
|
|
assert.ok(matches[0]);
|
|
assert.ok(Math.abs(matches[0].level - 0.4) < 1e-9, `median was ${matches[0].level}`);
|
|
});
|
|
|
|
test('a road with flow on only a short stretch (< half its samples) stays null', () => {
|
|
// Flow covers only the first ~15% of the road — 1 of 7 samples at best.
|
|
const stub = flow(line({ lengthM: 75, bearingDeg: 0, eastM: 2 }), 0.5);
|
|
const { matches, matchedCount, candidateCount } = matchFlowToRoads([ROAD_NS], [stub]);
|
|
assert.equal(matches[0], null);
|
|
assert.equal(matchedCount, 0);
|
|
assert.equal(candidateCount, 1); // it had candidates, they were just too few
|
|
});
|
|
|
|
test('multiple roads: results stay parallel to the input array', () => {
|
|
const roadFar = { coords: line({ eastM: 5000, lengthM: 500, bearingDeg: 0 }), type: 'residential' };
|
|
const flows = [flow(line({ lengthM: 500, bearingDeg: 0, eastM: 3 }), 0.6)];
|
|
const { matches, matchedCount } = matchFlowToRoads([roadFar, ROAD_NS], flows);
|
|
assert.equal(matches.length, 2);
|
|
assert.equal(matches[0], null);
|
|
assert.ok(matches[1]);
|
|
assert.equal(matchedCount, 1);
|
|
});
|
|
|
|
test('sparse-vertex flow polylines (long coord pairs) still match everywhere', () => {
|
|
// One 500 m segment as a single coord pair — midpoint hashing alone would
|
|
// miss samples near the ends; subdivision must cover them.
|
|
const sparse = flow([
|
|
ROAD_NS.coords[0],
|
|
ROAD_NS.coords[ROAD_NS.coords.length - 1],
|
|
], 0.5);
|
|
const { matches } = matchFlowToRoads([ROAD_NS], [sparse]);
|
|
assert.ok(matches[0], 'sparse flow polyline should match the full road');
|
|
});
|
|
|
|
// ── degenerate inputs ───────────────────────────────────────
|
|
|
|
test('empty inputs are safe', () => {
|
|
assert.deepEqual(matchFlowToRoads([], []), { matches: [], matchedCount: 0, candidateCount: 0 });
|
|
const noFlow = matchFlowToRoads([ROAD_NS], []);
|
|
assert.deepEqual(noFlow.matches, [null]);
|
|
assert.equal(noFlow.candidateCount, 0);
|
|
assert.deepEqual(matchFlowToRoads([], [flow(line({ lengthM: 100 }), 0.5)]).matches, []);
|
|
});
|
|
|
|
test('degenerate road (single/zero-length coords) stays null without throwing', () => {
|
|
const dot = { coords: [[LON0, LAT0]], type: 'residential' };
|
|
const zero = { coords: [[LON0, LAT0], [LON0, LAT0]], type: 'residential' };
|
|
const flows = [flow(line({ lengthM: 100 }), 0.5)];
|
|
const { matches } = matchFlowToRoads([dot, zero], flows);
|
|
assert.deepEqual(matches, [null, null]);
|
|
});
|
|
|
|
// ── median helper ───────────────────────────────────────────
|
|
|
|
test('median: odd count picks the middle', () => {
|
|
assert.equal(median([0.9, 0.2, 0.4]), 0.4);
|
|
});
|
|
|
|
test('median: even count averages the two middles', () => {
|
|
assert.equal(median([0.2, 0.4, 0.6, 1.0]), 0.5);
|
|
});
|
|
|
|
test('median: single value and empty', () => {
|
|
assert.equal(median([0.7]), 0.7);
|
|
assert.equal(median([]), null);
|
|
});
|
|
|
|
test('one-way rejects opposite closure and two travel directions retain separate congestion', () => {
|
|
const coords = [[0,0],[0.002,0]];
|
|
const forward = {coords, oneway:1};
|
|
const reverse = {coords, oneway:-1};
|
|
const east = {coords, trafficLevel:1, closure:false};
|
|
const west = {coords:[...coords].reverse(), trafficLevel:0, closure:true};
|
|
assert.equal(matchFlowToRoads([forward],[west]).matches[0], null);
|
|
for (const flows of [[east,west],[west,east]]) {
|
|
assert.deepEqual(matchFlowToRoads([forward,reverse],flows).matches,
|
|
[{level:1,closure:false},{level:0,closure:true}]);
|
|
}
|
|
assert.equal(matchFlowToRoads([forward],[east,{...east,trafficLevel:0,closure:true}]).matches[0], null);
|
|
});
|
|
|
|
test('a full-coverage flow line matches both travel directions; one-side does not', () => {
|
|
const coords = [[0,0],[0.002,0]];
|
|
const directions = [{coords, oneway:1}, {coords, oneway:-1}];
|
|
const full = {coords, trafficLevel:0.4, closure:false, coverage:'full'};
|
|
assert.deepEqual(matchFlowToRoads(directions, [full]).matches,
|
|
[{level:0.4,closure:false},{level:0.4,closure:false}]);
|
|
const oneSide = {...full, coverage:'one_side'};
|
|
assert.deepEqual(matchFlowToRoads(directions, [oneSide]).matches,
|
|
[{level:0.4,closure:false}, null]);
|
|
});
|