fix(traffic): stop a few matched roads reading as "no matches"

The live flow status derived its wording from the rounded coverage
percent, so 1 matched dot among 300 rounded to 0% and read
"SIMULATED ... (no matches)", and 1 simulated dot among 300 rounded to
100% and dropped "Unmatched: simulated". Coverage now stays within 1-99%
whenever both matched and simulated dots are shown.
This commit is contained in:
Aditya committed 2026-10-28 15:34:51 +05:30
1 parent a4d3701c3c
commit c6f437ed52
3 files changed
+38 -5

No files matched your search

+5 -3
View File
@@ -5810,9 +5810,11 @@ easier to meet (detection is now on more often), but does not create it.
OSM choice), so Hybrid fill is never replayed as OSM roads. Failed flow
refreshes clear old matches and report simulation. `flowCoveragePct` is the
percentage of shown dots on roads with flow, including all unmatched dots in
the denominator and excluding dots hidden by closures. The OSM keyed status
reads `LIVE · Roads: OpenStreetMap · Flow: TomTom · N% cov`. Road fetch
coverage follows a 5×5 ellipsoid sample of the central screen square.
the denominator and excluding dots hidden by closures. It reads 1–99 whenever
both matched and simulated dots are shown, so rounding never turns a few
matches into `(no matches)` or a few simulated dots into full coverage. The
OSM keyed status reads `LIVE · Roads: OpenStreetMap · Flow: TomTom · N% cov`.
Road fetch coverage follows a 5×5 ellipsoid sample of the central screen square.
Horizon rays are capped at 10 km from nadir (reduced if necessary to fit
16 z12 tiles); polygon-intersecting tiles load nearest first. Up to 16 z14
tiles provide near detail, with z12 roads beyond them. Cached tiles serve
+13 -2
View File
@@ -259,7 +259,8 @@ export function createControls({ state: layerState, services, parts, source }) {
* `error` carries this instant's health, so a live-configured layer whose
* flow feed went down reads DEGRADED with the reason instead of a stale
* LIVE coverage number. `flowCoveragePct` is shown dots on matched roads /
* all shown dots (0–100 int), excluding closures; `tilesFetched` counts flow-tile requests
* all shown dots (0–100 int, kept within 1–99 while both kinds are shown),
* excluding closures; `tilesFetched` counts flow-tile requests
* issued to the proxy this session (decode-cache hits excluded).
* @returns {{count:number, lastUpdate:number|null, loading:boolean,
* mode:'live'|'sim', error:string|null, flowCoveragePct:number,
@@ -304,8 +305,18 @@ export function createControls({ state: layerState, services, parts, source }) {
Boolean(layerState._heightPending);
const { free, slow, jam, sim } = layerState._bucketCounts;
const matched = free + slow + jam;
// The label reads 0 as "no matches" and 100 as nothing simulated, so
// rounding must not reach either while both kinds of dot are shown.
const flowCoveragePct =
matched + sim > 0 ? Math.round((100 * matched) / (matched + sim)) : 0;
matched + sim > 0
? Math.min(
sim > 0 ? 99 : 100,
Math.max(
matched > 0 ? 1 : 0,
Math.round((100 * matched) / (matched + sim)),
),
)
: 0;
const feed = parts.model.trafficFeedPresentation({
liveMode: layerState._liveMode,
fetching: loading,
+20
View File
@@ -516,6 +516,26 @@ test('late closures hide and stop dots; coverage counts shown matched dots, and
assert.equal(controls.getStats().flowCoveragePct, 80);
});
test('coverage never rounds to "no matches" or full coverage while both kinds of dot are shown', async () => {
const { state, controls } = await matchingContext(async () => []);
state._bucketCounts = { free: 1, slow: 0, jam: 0, sim: 300 };
assert.equal(controls.getStats().flowCoveragePct, 1);
assert.equal(
controls.getStats().loadingLabel,
'LIVE · Roads: OpenStreetMap · Flow: TomTom · 1% cov · Unmatched: simulated',
);
state._bucketCounts = { free: 300, slow: 0, jam: 0, sim: 1 };
assert.equal(controls.getStats().flowCoveragePct, 99);
assert.equal(
controls.getStats().loadingLabel,
'LIVE · Roads: OpenStreetMap · Flow: TomTom · 99% cov · Unmatched: simulated',
);
state._bucketCounts = { free: 300, slow: 0, jam: 0, sim: 0 };
assert.equal(controls.getStats().flowCoveragePct, 100);
state._bucketCounts = { free: 0, slow: 0, jam: 0, sim: 300 };
assert.equal(controls.getStats().flowCoveragePct, 0);
});
test('refresh failure removes cached matches and reports simulated flow', async () => {
const { flow, state, model, controls } = await matchingContext(async () => {
throw new Error('HTTP 429');