mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Merge remote-tracking branch 'origin/main' into fix/hal-722-radio-next-no-autoplay
This commit is contained in:
commit
238a3d0634
465 files changed
+26290
-1061
No files matched your search
@@ -10,11 +10,16 @@
|
||||
// Run with: npm test
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { rm } from 'node:fs/promises';
|
||||
import { gbfsProxy } from '../../../server/providers/gbfs.js';
|
||||
import { localProviderPlugins } from '../../../server/providers/local.js';
|
||||
import { createBikeshareSource } from './source.js';
|
||||
import { createModel } from './model.js';
|
||||
import { CITY_BY_ID } from './registry.js';
|
||||
import { makeFixtureRoot } from '../../tooling/fixtureRoot.mjs';
|
||||
|
||||
const nominatimStorageDir = await makeFixtureRoot('gev-gbfs-composition-');
|
||||
test.after(() => rm(nominatimStorageDir, { recursive: true, force: true }));
|
||||
|
||||
const AUSTIN = CITY_BY_ID.get('austin-capmetro');
|
||||
const OSLO = CITY_BY_ID.get('oslo-bysykkel');
|
||||
@@ -339,7 +344,7 @@ test('the mounted route still refuses a host outside the allowlist', async () =>
|
||||
* or from the preview half of it — fails here rather than in a browser.
|
||||
*/
|
||||
function composedGbfsPlugin() {
|
||||
const plugin = localProviderPlugins().find(
|
||||
const plugin = localProviderPlugins({ nominatimStorageDir }).find(
|
||||
(entry) => entry.name === 'gbfs-proxy',
|
||||
);
|
||||
assert.ok(plugin, 'the application must compose a GBFS provider');
|
||||
@@ -446,3 +451,62 @@ test('unknown GBFS counts stay unknown through the source, model and station col
|
||||
assert.equal(model.toNonNegativeInteger('3'), 3);
|
||||
}
|
||||
});
|
||||
|
||||
test('a station without a published capacity re-derives it from every status poll', async () => {
|
||||
const { createRendering } = await import('./rendering.js');
|
||||
const { COLOR_YELLOW } = await import('./policy.js');
|
||||
const model = createModel({});
|
||||
const state = {
|
||||
_cityRuntime: new Map(),
|
||||
_stationRenderMap: new Map(),
|
||||
_pointCollection: { add: (options) => ({ ...options }) },
|
||||
};
|
||||
const rendering = createRendering({
|
||||
state,
|
||||
services: { render: { governorRequestRender() {} } },
|
||||
parts: { model },
|
||||
});
|
||||
const info = model.parseStationInformation({
|
||||
data: {
|
||||
stations: [
|
||||
{ station_id: 'open', name: 'Open', lat: 30.26, lon: -97.74 },
|
||||
{
|
||||
station_id: 'fixed',
|
||||
name: 'Fixed',
|
||||
lat: 30.27,
|
||||
lon: -97.75,
|
||||
capacity: 13,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
rendering.ensureCityPoints('austin', info);
|
||||
const poll = (stations) =>
|
||||
rendering.applyStatusToPoints(
|
||||
'austin',
|
||||
model.parseStationStatus({ data: { stations } }),
|
||||
);
|
||||
const open = state._stationRenderMap.get('austin:open');
|
||||
const fixed = state._stationRenderMap.get('austin:fixed');
|
||||
|
||||
// The first poll has no row for the station: capacity is a placeholder.
|
||||
poll([
|
||||
{
|
||||
station_id: 'fixed',
|
||||
num_bikes_available: 4,
|
||||
num_docks_available: 9,
|
||||
},
|
||||
]);
|
||||
poll([
|
||||
{ station_id: 'open', num_bikes_available: 10, num_docks_available: 20 },
|
||||
{ station_id: 'fixed', num_bikes_available: 2, num_docks_available: 3 },
|
||||
]);
|
||||
assert.equal(open.capacity, 30, 'the first poll fixed the capacity');
|
||||
assert.equal(open.point.color, COLOR_YELLOW, '10 of 30 is not plenty');
|
||||
assert.equal(fixed.capacity, 13, 'a published capacity is kept');
|
||||
|
||||
poll([
|
||||
{ station_id: 'open', num_bikes_available: 4, num_docks_available: 2 },
|
||||
]);
|
||||
assert.equal(open.capacity, 6);
|
||||
});
|
||||
@@ -185,6 +185,7 @@ export function createRendering({
|
||||
stationName: station.name,
|
||||
point,
|
||||
capacity: parts.model.toNonNegativeInteger(station.capacity),
|
||||
publishedCapacity: parts.model.toNonNegativeInteger(station.capacity),
|
||||
bikesAvailable: null,
|
||||
docksAvailable: null,
|
||||
isInstalled: station.isInstalled,
|
||||
@@ -245,7 +246,8 @@ export function createRendering({
|
||||
if (!record) continue;
|
||||
|
||||
const status = statusMap.get(record.stationId) || null;
|
||||
const capacity = resolveCapacity(record.capacity, status);
|
||||
// Resolve from the published capacity, not last poll's derived value.
|
||||
const capacity = resolveCapacity(record.publishedCapacity, status);
|
||||
record.capacity = capacity;
|
||||
|
||||
record.bikesAvailable = parts.model.toNonNegativeInteger(
|
||||
|
||||
@@ -4,10 +4,7 @@ import {
|
||||
militaryNamesInView,
|
||||
nameMilitaryFragment,
|
||||
} from '../../data/militaryNames.js';
|
||||
import {
|
||||
createOpenFreeMapSource,
|
||||
decodeOpenFreeMapMilitaryTile,
|
||||
} from '../../sources/openFreeMap.js';
|
||||
import { createOpenFreeMapSource } from '../../sources/openFreeMap.js';
|
||||
import {
|
||||
mergeMilitaryFragments,
|
||||
militaryNameInView,
|
||||
@@ -96,7 +93,7 @@ export function createInstallationSource({
|
||||
loadNames = loadMilitaryNames,
|
||||
mapTiles = createOpenFreeMapSource({
|
||||
fetchImpl: tileFetchImpl,
|
||||
decode: decodeOpenFreeMapMilitaryTile,
|
||||
selectDecoded: ({ military }) => ({ military }),
|
||||
}),
|
||||
} = {}) {
|
||||
let overpassUnavailable = false;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
MILITARY_TILE_MIN_ZOOM,
|
||||
} from './source.js';
|
||||
import {
|
||||
clearSharedOpenFreeMapTiles,
|
||||
decodeOpenFreeMapMilitaryTile,
|
||||
decodeOpenFreeMapTile,
|
||||
} from '../../sources/openFreeMap.js';
|
||||
@@ -224,6 +225,7 @@ test('the military-only decoder keeps Camp Mabry and skips road geometry', () =>
|
||||
});
|
||||
|
||||
test('installation API transports never receive vector-tile URLs', async (t) => {
|
||||
t.after(clearSharedOpenFreeMapTiles);
|
||||
const calls = [];
|
||||
const tileFetch = async (url) => {
|
||||
calls.push(url);
|
||||
@@ -242,6 +244,7 @@ test('installation API transports never receive vector-tile URLs', async (t) =>
|
||||
};
|
||||
t.mock.method(globalThis, 'fetch', tileFetch);
|
||||
for (const tileFetchImpl of [undefined, tileFetch]) {
|
||||
clearSharedOpenFreeMapTiles();
|
||||
const source = createInstallationSource({
|
||||
tileFetchImpl,
|
||||
fetchImpl: async (url) => {
|
||||
|
||||
@@ -6,6 +6,21 @@ import {
|
||||
} from './geometry.js';
|
||||
import * as Cesium from 'cesium';
|
||||
|
||||
const SOURCE_FEATURE_INDEX = '__gevSourceFeatureIndex';
|
||||
|
||||
function indexedCollection(features) {
|
||||
return {
|
||||
type: 'FeatureCollection',
|
||||
features: features.map((feature, index) => ({
|
||||
...feature,
|
||||
properties: {
|
||||
...feature.properties,
|
||||
[SOURCE_FEATURE_INDEX]: index,
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function createIngestion({ state, parts, source }) {
|
||||
async function load(viewer) {
|
||||
if (state._loading || state._loaded) return;
|
||||
@@ -49,7 +64,7 @@ export function createIngestion({ state, parts, source }) {
|
||||
const landingFeatures = normalizeFeatures(landingJson, 'landing');
|
||||
|
||||
const cableDataSource = await Cesium.GeoJsonDataSource.load(
|
||||
{ type: 'FeatureCollection', features: cableFeatures },
|
||||
indexedCollection(cableFeatures),
|
||||
{
|
||||
clampToGround: true,
|
||||
stroke: state.cableColor.withAlpha(0.95),
|
||||
@@ -61,7 +76,7 @@ export function createIngestion({ state, parts, source }) {
|
||||
);
|
||||
if (!owns()) return;
|
||||
const landingDataSource = await Cesium.GeoJsonDataSource.load(
|
||||
{ type: 'FeatureCollection', features: landingFeatures },
|
||||
indexedCollection(landingFeatures),
|
||||
{
|
||||
clampToGround: true,
|
||||
stroke: state.landingColor.withAlpha(0.9),
|
||||
@@ -131,7 +146,12 @@ export function createIngestion({ state, parts, source }) {
|
||||
state._referenceRecords = [];
|
||||
state._surfaceRecords = [];
|
||||
|
||||
cableEntities.forEach((entity, index) => {
|
||||
// Cesium expands multipart geometry into several entities and suffixes
|
||||
// colliding IDs. Its copied properties retain this source index even
|
||||
// when a provider has no ID, so every segment keeps the right owner.
|
||||
const cableReferences = new Set();
|
||||
cableEntities.forEach((entity) => {
|
||||
const index = entity.properties?.[SOURCE_FEATURE_INDEX]?.getValue();
|
||||
const feature = cableFeatures[index];
|
||||
const reference = featureReference(feature);
|
||||
if (!reference) return;
|
||||
@@ -146,6 +166,8 @@ export function createIngestion({ state, parts, source }) {
|
||||
entity,
|
||||
base: Cesium.Cartesian3.fromDegrees(reference.lon, reference.lat, 0),
|
||||
});
|
||||
if (cableReferences.has(index)) return;
|
||||
cableReferences.add(index);
|
||||
parts.rendering.addReferenceStem({
|
||||
reference,
|
||||
label: featureLabel(feature),
|
||||
@@ -155,7 +177,9 @@ export function createIngestion({ state, parts, source }) {
|
||||
});
|
||||
});
|
||||
|
||||
landingEntities.forEach((entity, index) => {
|
||||
const landingReferences = new Set();
|
||||
landingEntities.forEach((entity) => {
|
||||
const index = entity.properties?.[SOURCE_FEATURE_INDEX]?.getValue();
|
||||
const feature = landingFeatures[index];
|
||||
const reference = featureReference(feature);
|
||||
if (!reference) return;
|
||||
@@ -170,6 +194,8 @@ export function createIngestion({ state, parts, source }) {
|
||||
entity,
|
||||
base: Cesium.Cartesian3.fromDegrees(reference.lon, reference.lat, 0),
|
||||
});
|
||||
if (landingReferences.has(index)) return;
|
||||
landingReferences.add(index);
|
||||
parts.rendering.addReferenceStem({
|
||||
reference,
|
||||
label: featureLabel(feature),
|
||||
|
||||
@@ -154,57 +154,200 @@ for (const targetHeight of [200, 2000]) test(`visible height steps stay pixel-li
|
||||
assert.equal(state._heightPending, true);
|
||||
});
|
||||
|
||||
for (const outcome of ['timeout', 'abort']) test(`a road pass ${outcome} invalidates queued tiles and terrain publication without a newer generation`, async (t) => {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||||
const held = [];
|
||||
const setTimer = globalThis.setTimeout;
|
||||
t.mock.method(globalThis, 'setTimeout', (fn, delay, ...args) => delay === 0
|
||||
? (held.push(fn), held.length) : setTimer(fn, delay, ...args));
|
||||
let clock = 0;
|
||||
t.mock.method(performance, 'now', () => clock += 13);
|
||||
function holdFlowTasks(t) {
|
||||
const yields = [];
|
||||
const scheduler = Object.getOwnPropertyDescriptor(globalThis, 'scheduler');
|
||||
Object.defineProperty(globalThis, 'scheduler', {
|
||||
configurable: true,
|
||||
value: { yield: () => new Promise((resolve) => yields.push(resolve)) },
|
||||
});
|
||||
t.after(() => {
|
||||
for (const resolve of yields) resolve();
|
||||
if (scheduler) Object.defineProperty(globalThis, 'scheduler', scheduler);
|
||||
else delete globalThis.scheduler;
|
||||
});
|
||||
return async () => {
|
||||
assert.ok(yields.length, 'pass reached a flow task boundary');
|
||||
yields.shift()();
|
||||
await flush();
|
||||
};
|
||||
}
|
||||
|
||||
function flowFixture(source) {
|
||||
const state = createState({ services: {} });
|
||||
let options, samples = 0;
|
||||
const road = { type: 'primary', oneway: 1, coords: [[0, 0], [0.001, 0]],
|
||||
waypoints: [C.Cartesian3.fromDegrees(0, 0), C.Cartesian3.fromDegrees(0.001, 0)], segmentDist: [111] };
|
||||
const source = { getStatus: async () => ({ hasKey: false }), requestRoads: (_b, opts) => {
|
||||
options = opts;
|
||||
opts.onTile({ roads: [road] });
|
||||
return new Promise(() => {});
|
||||
} };
|
||||
const calls = { samples: 0, selections: 0 };
|
||||
const road = {
|
||||
type: 'primary',
|
||||
oneway: 1,
|
||||
coords: [
|
||||
[0, 0],
|
||||
[0.001, 0],
|
||||
],
|
||||
waypoints: [
|
||||
C.Cartesian3.fromDegrees(0, 0),
|
||||
C.Cartesian3.fromDegrees(0.001, 0),
|
||||
],
|
||||
segmentDist: [111],
|
||||
};
|
||||
const paints = [];
|
||||
Object.assign(state, { _enabled: true, _roadMode: 'osm', _parseRoads: (data) => data.roads,
|
||||
_viewer: { scene: { globe: { show: true, getHeight: () => { samples++; return 100; } } } } });
|
||||
Object.assign(state, {
|
||||
_enabled: true,
|
||||
_roadMode: 'osm',
|
||||
_parseRoads: (data) => data.roads,
|
||||
_viewer: {
|
||||
scene: {
|
||||
globe: {
|
||||
show: true,
|
||||
getHeight: () => {
|
||||
calls.samples++;
|
||||
return 100;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const parts = {
|
||||
viewport: { clampBounds: (b) => b, getBoundsCenter: () => ({ lat: 0, lon: 0 }), onCameraChanged() {} },
|
||||
rendering: { visibleRoadsForAltitude: (roads) => roads, renderRoadsForAltitude: (...args) => paints.push(args) },
|
||||
model: { allocateRoadDotBudgets: (roads) => roads.map(() => 1), recolorDotsInPlace() {} },
|
||||
viewport: {
|
||||
clampBounds: (b) => b,
|
||||
getBoundsCenter: () => ({ lat: 0, lon: 0 }),
|
||||
onCameraChanged() {},
|
||||
},
|
||||
rendering: {
|
||||
visibleRoadsForAltitude: (roads) => {
|
||||
calls.selections++;
|
||||
return roads;
|
||||
},
|
||||
renderRoadsForAltitude: (...args) => paints.push(args),
|
||||
},
|
||||
model: {
|
||||
allocateRoadDotBudgets: (roads) => roads.map(() => 1),
|
||||
recolorDotsInPlace() {},
|
||||
},
|
||||
};
|
||||
const ctx = { state, source, services: { credits: {} }, parts };
|
||||
parts.flow = createFlow(ctx);
|
||||
const ingestion = createIngestion(ctx);
|
||||
const request = ingestion.loadRoadsForBounds(bounds, 450);
|
||||
await flush();
|
||||
assert.equal(held.length, 1);
|
||||
held.shift()(); await flush();
|
||||
assert.equal(held.length, 1, 'held at the final station before the real terrain onReady callback');
|
||||
options.onTile({ roads: [road] });
|
||||
const generation = state._loadGeneration;
|
||||
const revision = state._surfaceRevision;
|
||||
const samplesBefore = samples;
|
||||
if (outcome === 'timeout') t.mock.timers.tick(12001);
|
||||
else ingestion.cancelActiveFetch();
|
||||
await request;
|
||||
assert.equal(options.signal.aborted, true);
|
||||
assert.equal(state._loadGeneration, generation);
|
||||
assert.equal(state._fetching, false);
|
||||
if (outcome === 'timeout') {
|
||||
assert.equal(state._activeFetchAbort.signal.aborted, false);
|
||||
assert.match(state._roadError, /timed out/);
|
||||
assert.equal(state._retryAttempts, 1);
|
||||
}
|
||||
held.shift()(); await flush();
|
||||
assert.equal(paints.length, 0, 'expired terrain cannot publish');
|
||||
assert.equal(samples, samplesBefore, 'queued tiles cannot start surface preparation');
|
||||
assert.equal(state._surfaceRevision, revision, 'queued tiles cannot invalidate another surface pass');
|
||||
ingestion.cancelActiveFetch();
|
||||
});
|
||||
return { state, road, paints, parts, ctx, calls };
|
||||
}
|
||||
|
||||
for (const outcome of ['timeout', 'abort'])
|
||||
test(`a road pass ${outcome} invalidates queued tiles and terrain publication without a newer generation`, async (t) => {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||||
const advanceFlowTask = holdFlowTasks(t);
|
||||
const held = [];
|
||||
const setTimer = globalThis.setTimeout;
|
||||
t.mock.method(globalThis, 'setTimeout', (fn, delay, ...args) =>
|
||||
delay === 0 ? (held.push(fn), held.length) : setTimer(fn, delay, ...args),
|
||||
);
|
||||
let clock = 0;
|
||||
t.mock.method(performance, 'now', () => (clock += 13));
|
||||
let options;
|
||||
const source = {
|
||||
getStatus: async () => ({ hasKey: false }),
|
||||
requestRoads: (_b, opts) => {
|
||||
options = opts;
|
||||
opts.onTile({ roads: [road] });
|
||||
return new Promise(() => {});
|
||||
},
|
||||
};
|
||||
const { state, road, paints, ctx, calls } = flowFixture(source);
|
||||
const ingestion = createIngestion(ctx);
|
||||
const request = ingestion.loadRoadsForBounds(bounds, 450);
|
||||
await flush();
|
||||
await advanceFlowTask();
|
||||
const samplesBeforeSurface = calls.samples;
|
||||
await advanceFlowTask();
|
||||
assert.equal(held.length, 1);
|
||||
held.shift()();
|
||||
await flush();
|
||||
assert.equal(
|
||||
held.length,
|
||||
1,
|
||||
'held at the final station before the real terrain onReady callback',
|
||||
);
|
||||
assert.equal(
|
||||
calls.samples - samplesBeforeSurface,
|
||||
road.coords.length,
|
||||
'both terrain stations were sampled before the final hold',
|
||||
);
|
||||
options.onTile({ roads: [road] });
|
||||
const generation = state._loadGeneration;
|
||||
const revision = state._surfaceRevision;
|
||||
const samplesBefore = calls.samples;
|
||||
if (outcome === 'timeout') t.mock.timers.tick(12001);
|
||||
else ingestion.cancelActiveFetch();
|
||||
await request;
|
||||
assert.equal(options.signal.aborted, true);
|
||||
assert.equal(state._loadGeneration, generation);
|
||||
assert.equal(state._fetching, false);
|
||||
if (outcome === 'timeout') {
|
||||
assert.equal(state._activeFetchAbort.signal.aborted, false);
|
||||
assert.match(state._roadError, /timed out/);
|
||||
assert.equal(state._retryAttempts, 1);
|
||||
}
|
||||
held.shift()();
|
||||
await flush();
|
||||
assert.equal(paints.length, 0, 'expired terrain cannot publish');
|
||||
assert.equal(
|
||||
calls.samples,
|
||||
samplesBefore,
|
||||
'queued tiles cannot start surface preparation',
|
||||
);
|
||||
assert.equal(
|
||||
state._surfaceRevision,
|
||||
revision,
|
||||
'queued tiles cannot invalidate another surface pass',
|
||||
);
|
||||
ingestion.cancelActiveFetch();
|
||||
});
|
||||
|
||||
for (const checkpoint of ['none', 'prepare', 'surface'])
|
||||
test(
|
||||
checkpoint === 'none'
|
||||
? 'a current surface pass crosses both task boundaries'
|
||||
: `a newer surface revision stops stale ${checkpoint} work without aborting the pass`,
|
||||
async (t) => {
|
||||
// These cases exercise ownership boundaries, not the surface slice budget.
|
||||
t.mock.method(performance, 'now', () => 0);
|
||||
const advanceFlowTask = holdFlowTasks(t);
|
||||
const { state, road, paints, parts, calls } = flowFixture({});
|
||||
const controller = new AbortController();
|
||||
t.after(() => controller.abort());
|
||||
const generation = state._loadGeneration;
|
||||
const pass = parts.flow.applyFlowThenRender(
|
||||
[road],
|
||||
bounds,
|
||||
generation,
|
||||
450,
|
||||
'Loaded tile',
|
||||
null,
|
||||
false,
|
||||
controller.signal,
|
||||
);
|
||||
await flush();
|
||||
if (checkpoint === 'prepare') state._surfaceRevision++;
|
||||
await advanceFlowTask();
|
||||
if (checkpoint === 'prepare') {
|
||||
assert.equal(
|
||||
calls.selections,
|
||||
0,
|
||||
'stale whole-graph preparation cannot start',
|
||||
);
|
||||
} else {
|
||||
assert.ok(calls.selections > 0, 'the healthy preparation prefix ran');
|
||||
if (checkpoint === 'surface') state._surfaceRevision++;
|
||||
await advanceFlowTask();
|
||||
}
|
||||
const rendered = await pass;
|
||||
assert.equal(state._loadGeneration, generation);
|
||||
assert.equal(controller.signal.aborted, false);
|
||||
if (checkpoint === 'none') {
|
||||
assert.equal(rendered, true);
|
||||
assert.ok(calls.samples > 0);
|
||||
assert.ok(paints.length > 0);
|
||||
} else {
|
||||
assert.equal(rendered, false);
|
||||
assert.equal(calls.samples, 0, 'superseded work cannot sample terrain');
|
||||
assert.equal(paints.length, 0, 'superseded work cannot publish');
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
roadDotBudget,
|
||||
} from './policy.js';
|
||||
import { msg } from '../../i18n/index.js';
|
||||
import { nextTask } from '../../sources/vectorTiles.js';
|
||||
|
||||
export function createFlow({ state: layerState, services, parts, source }) {
|
||||
const { registerDynamicCredit, TOMTOM_CREDIT } = services.credits;
|
||||
@@ -253,6 +254,9 @@ export function createFlow({ state: layerState, services, parts, source }) {
|
||||
parts.rendering.renderRoadsForAltitude([], altitude, label, trace);
|
||||
return true;
|
||||
}
|
||||
// Separate whole-graph preparation from tile completion and publication.
|
||||
await nextTask();
|
||||
if (!current()) return false;
|
||||
const scene = layerState._viewer.scene;
|
||||
// The enable-time estimate can predate the first local mesh (especially
|
||||
// after an intercontinental jump). Once previews have measured roads, use
|
||||
@@ -476,6 +480,9 @@ export function createFlow({ state: layerState, services, parts, source }) {
|
||||
if (!lastPaint || performance.now() - lastPaint >= 120) paint();
|
||||
},
|
||||
};
|
||||
// The first bounded surface slice needs its own task, not the prefix's tail.
|
||||
await nextTask();
|
||||
if (!current()) return false;
|
||||
let prepared = await prepareRoadSurfaces(
|
||||
unresolved,
|
||||
layerState._viewer.scene,
|
||||
|
||||
@@ -76,6 +76,10 @@ function setup(t, requestRoads, getStatus = async () => ({ hasKey: false })) {
|
||||
const tick = async (ms) => {
|
||||
t.mock.timers.tick(ms);
|
||||
for (let i = 0; i < 30; i++) await Promise.resolve();
|
||||
t.mock.timers.tick(0);
|
||||
for (let i = 0; i < 30; i++) await Promise.resolve();
|
||||
t.mock.timers.tick(0);
|
||||
for (let i = 0; i < 30; i++) await Promise.resolve();
|
||||
};
|
||||
return { layer, viewer, move, tick };
|
||||
}
|
||||
|
||||
@@ -84,7 +84,10 @@ export function roadRequestError(status, cause) {
|
||||
export function createTrafficSource({
|
||||
fetchImpl = (...args) => globalThis.fetch(...args),
|
||||
tileFetchImpl = (...args) => globalThis.fetch(...args),
|
||||
mapTiles = createOpenFreeMapSource({ fetchImpl: tileFetchImpl }),
|
||||
mapTiles = createOpenFreeMapSource({
|
||||
fetchImpl: tileFetchImpl,
|
||||
selectDecoded: ({ roads }) => ({ roads }),
|
||||
}),
|
||||
} = {}) {
|
||||
const flow = createFlowTileSource({ fetchImpl });
|
||||
const api = {
|
||||
|
||||
@@ -5,7 +5,10 @@ import { getEventListeners } from 'node:events';
|
||||
import { createFlowTileSource } from './flowSource.js';
|
||||
import { tilesForBounds, tileToBBox } from '../../data/tomtomTiles.js';
|
||||
import { decodeFlowTile } from './flowDecode.js';
|
||||
import { decodeOpenFreeMapTile } from '../../sources/openFreeMap.js';
|
||||
import {
|
||||
clearSharedOpenFreeMapTiles,
|
||||
decodeOpenFreeMapTile,
|
||||
} from '../../sources/openFreeMap.js';
|
||||
import {
|
||||
RoadRequestError,
|
||||
roadRequestError,
|
||||
@@ -851,6 +854,8 @@ test('failed detail cores retain real coarse roads while successful cores replac
|
||||
});
|
||||
|
||||
test('traffic separates API and public tile transports, including the default tile fetch', async (t) => {
|
||||
clearSharedOpenFreeMapTiles();
|
||||
t.after(clearSharedOpenFreeMapTiles);
|
||||
const publicUrls = [],
|
||||
apiUrls = [];
|
||||
const publicFetch = async (url) => {
|
||||
@@ -886,7 +891,12 @@ test('traffic separates API and public tile transports, including the default ti
|
||||
(await (await source.requestOsmRoads(bounds)).json()).roads.length,
|
||||
);
|
||||
}
|
||||
assert.ok(publicUrls.length >= 4);
|
||||
assert.equal(
|
||||
publicUrls.filter((url) => url.endsWith('.pbf')).length,
|
||||
1,
|
||||
'compatible sources reuse the shared decoded public tile',
|
||||
);
|
||||
assert.equal(publicUrls.filter((url) => url.endsWith('/planet')).length, 2);
|
||||
assert.ok(apiUrls.length >= 4);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user