Files
gods-eye-view/src/layers/traffic/roadModes.test.mjs
T

743 lines
25 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { PbfWriter } from 'pbf';
import {
flowSegmentsToRoads,
resolveRoadMode,
selectTrafficRoads,
} from './roadModes.js';
import { createFlowTileSource } from './flowSource.js';
import { createTrafficSource } from './source.js';
import { createTrafficLayer } from './index.js';
import {
createDefaultLayerState,
encodeLayerStateParams,
decodeLayerStateParams,
serializeStoredLayerState,
parseStoredLayerState,
} from '../../data/layerState.js';
import { tileToBBox } from '../../data/tomtomTiles.js';
const coordinates = [
[-97.744, 30.267],
[-97.742, 30.267],
];
const main = { coordinates, type: 'primary', oneway: 1 };
const side = {
coordinates: [
[-97.743, 30.267],
[-97.743, 30.269],
],
type: 'residential',
oneway: 1,
};
const flow = {
coords: coordinates,
trafficLevel: 0.3,
closure: false,
roadType: 'Major road',
};
const box = { west: -97.75, east: -97.74, south: 30.26, north: 30.27 };
test('road modes have key-aware defaults, explicit overrides and keyless fallback', () => {
assert.equal(resolveRoadMode(null, true), 'hybrid');
assert.equal(resolveRoadMode(null, false), 'osm');
assert.equal(resolveRoadMode('invalid', true), 'hybrid');
for (const mode of ['tomtom', 'osm', 'hybrid']) {
assert.equal(resolveRoadMode(mode, true), mode);
assert.equal(resolveRoadMode(mode, false), 'osm');
}
});
test('Hybrid keeps TomTom and the unmatched OSM side street, with simulated fill', () => {
const roads = selectTrafficRoads([main, side], [flow], 'hybrid');
assert.equal(roads.length, 2);
assert.equal(roads[0].directFlow, true);
assert.equal(roads[0].flow.level, 0.3);
assert.equal(roads[1].simulatedOnly, true);
assert.deepEqual(roads[1].coordinates, side.coordinates);
assert.equal(selectTrafficRoads([main, side], [flow], 'tomtom').length, 1);
assert.deepEqual(selectTrafficRoads([main, side], [flow], 'osm'), [
main,
side,
]);
});
test('opposite-direction carriageways and the reverse half of two-way roads survive', () => {
for (const reverse of [
{ ...main, oneway: -1 },
{ ...main, coordinates: [...coordinates].reverse() },
]) {
assert.equal(selectTrafficRoads([reverse], [flow], 'hybrid').length, 2);
}
const twoWay = selectTrafficRoads([{ ...main, oneway: 0 }], [flow], 'hybrid');
assert.equal(twoWay.length, 2);
assert.equal(twoWay[1].oneway, -1);
assert.equal(twoWay[1].densityWeight, 0.5);
});
test('uncertain flow values still dedupe OSM; every TomTom road and closure survives', () => {
const closed = { ...flow, trafficLevel: 0, closure: true };
const roads = selectTrafficRoads([main], [flow, closed], 'hybrid');
assert.equal(roads.length, 2);
assert.ok(roads.every((road) => road.directFlow));
assert.equal(roads[1].flow.closure, true);
});
test('a partly covered OSM road keeps only its uncovered stretch', () => {
// 0.004° of longitude at 30.267° N is about 385 m; TomTom covers the west half.
const long = {
coordinates: [
[-97.744, 30.267],
[-97.74, 30.267],
],
type: 'primary',
oneway: 1,
};
const roads = selectTrafficRoads([long], [flow], 'hybrid');
assert.equal(roads.length, 2);
const [piece] = roads.slice(1);
assert.equal(piece.simulatedOnly, true);
// The kept stretch starts within one 35 m match radius of the TomTom end.
assert.ok(piece.coordinates[0][0] > -97.742);
assert.ok(piece.coordinates[0][0] < -97.741);
assert.deepEqual(piece.coordinates.at(-1), [-97.74, 30.267]);
});
test('an uncovered sliver shorter than 40 m is dropped with its duplicate', () => {
// About 67 m past the TomTom end: the last ~32 m is beyond the 35 m radius.
const sliver = {
coordinates: [
[-97.744, 30.267],
[-97.7413, 30.267],
],
type: 'primary',
oneway: 1,
};
assert.equal(selectTrafficRoads([sliver], [flow], 'hybrid').length, 1);
});
test('a full-coverage TomTom line is two-way and covers both OSM directions', () => {
const both = { ...flow, coverage: 'full' };
assert.equal(flowSegmentsToRoads([both])[0].oneway, 0);
assert.equal(flowSegmentsToRoads([flow])[0].oneway, 1);
const roads = selectTrafficRoads([{ ...main, oneway: 0 }], [both], 'hybrid');
assert.equal(roads.length, 1);
assert.equal(roads[0].directFlow, true);
});
test("opposite TomTom sides of one street share a two-way street's dots", () => {
// About 1 m and 20 m north of `coordinates`, drawn westbound.
const back = (dLat) => ({
...flow,
coords: coordinates.map(([lon, lat]) => [lon, lat + dLat]).reverse(),
});
const weights = (segments) =>
flowSegmentsToRoads(segments).map((road) => road.densityWeight ?? 1);
assert.deepEqual(weights([flow, back(0.00001)]), [0.5, 0.5]);
assert.deepEqual(weights([flow]), [1], 'a lone side keeps the one-way share');
assert.deepEqual(
weights([flow, back(0.00018)]),
[1, 1],
'a divided carriageway keeps one-way shares',
);
assert.deepEqual(
weights([
flow,
{
...flow,
coords: coordinates.map(([lon, lat]) => [lon, lat + 0.00001]),
},
]),
[1, 1],
'same-direction lines are not a pair',
);
assert.deepEqual(
weights([{ ...flow, coverage: 'full' }, back(0.00001)]),
[1, 1],
'a full-coverage line is not a side, and pairs with nothing',
);
});
test('a TomTom side paired along only part of its length keeps its share', () => {
// Westbound partners about 1 m north, from the start of `flow` to 30% and
// to 60% of its length: one or two of its three sample points.
const partner = (toLon) => ({
...flow,
coords: [
[toLon, 30.26701],
[-97.744, 30.26701],
],
});
const weightOfFlow = (other) =>
flowSegmentsToRoads([flow, other])[0].densityWeight ?? 1;
assert.equal(weightOfFlow(partner(-97.7434)), 1);
assert.equal(weightOfFlow(partner(-97.7428)), 0.5);
});
test('a TomTom side that doubles back beside itself is not its own pair', () => {
// East along `coordinates`, then back west about 1 m north.
const hairpin = {
...flow,
coords: [...coordinates, [-97.742, 30.26701], [-97.744, 30.26701]],
};
assert.equal(flowSegmentsToRoads([hairpin])[0].densityWeight ?? 1, 1);
});
/** A flow tile of one-way Major roads; each line is [x, y, x, y, ...]. */
function flowTile(lines) {
const writer = new PbfWriter();
writer.writeMessage(3, writeFlowLayer, lines);
return writer.finish();
}
function writeFlowLayer(lines, layer) {
layer.writeVarintField(15, 2);
layer.writeStringField(1, 'Traffic flow');
layer.writeVarintField(5, 4096);
layer.writeStringField(3, 'traffic_level');
layer.writeMessage(4, (_, value) => value.writeDoubleField(3, 0.6), null);
layer.writeStringField(3, 'road_type');
layer.writeMessage(
4,
(_, value) => value.writeStringField(1, 'Major road'),
null,
);
layer.writeStringField(3, 'traffic_road_coverage');
layer.writeMessage(
4,
(_, value) => value.writeStringField(1, 'one_side'),
null,
);
for (const xy of lines) {
// MoveTo one point, LineTo the rest, as zigzag deltas.
const geometry = [9];
for (let i = 0; i < xy.length; i++) {
if (i === 2) geometry.push(2 | ((xy.length / 2 - 1) << 3));
const delta = xy[i] - (i < 2 ? 0 : xy[i - 2]);
geometry.push((delta << 1) ^ (delta >> 31));
}
layer.writeMessage(
2,
(_, feature) => {
feature.writeVarintField(3, 2);
feature.writePackedVarint(2, [0, 0, 1, 1, 2, 2]);
feature.writePackedVarint(4, geometry);
},
null,
);
}
}
test('TomTom sides clipped by their tile and the view pair only with other lines', async () => {
// A one-way hairpin turning in the tile buffer, its arms one tile unit
// (about 2 m) apart, and a separate opposite pair. The tile clip cuts the
// hairpin into two arms; the view clip then cuts every line again.
const lines = [
[500, 1000, 4300, 1000, 4300, 1001, 500, 1001],
[-200, 3000, 4300, 3000],
[4300, 3001, -200, 3001],
];
const source = createFlowTileSource({
fetchImpl: async () => new Response(flowTile(lines)),
});
const tile = tileToBBox(12, 935, 1686);
const west = tile.west + (tile.east - tile.west) * 0.2;
const { segments } = await source.fetchFlowDetail({
...tile,
west,
east: tile.east - 1e-9,
south: tile.south + 1e-9,
});
assert.equal(segments.length, 4);
assert.ok(
segments.every((s) => s.coords.every(([lon]) => lon >= west - 1e-10)),
);
assert.deepEqual(
flowSegmentsToRoads(segments).map((road) => road.densityWeight ?? 1),
[1, 1, 0.5, 0.5],
);
});
test('bearing and distance reject nearby crossing and separate OSM roads', () => {
const parallel = {
...main,
coordinates: coordinates.map(([lon, lat]) => [lon, lat + 0.001]),
};
assert.equal(
selectTrafficRoads([side, parallel], [flow], 'hybrid').length,
3,
);
});
function sourceFixture() {
let requests = 0;
const source = createTrafficSource({
mapTiles: {
async fetchBounds(bounds, { onTile }) {
requests++;
const tile = { roads: [main, side] };
onTile?.(tile);
return { tiles: [tile], partial: false };
},
clear() {},
},
});
return { source, requests: () => requests };
}
test('TomTom-only skips OSM requests; keyless selection requests OSM and labels fallback', async () => {
for (const hasKey of [true, false]) {
const f = sourceFixture();
const data = await (
await f.source.requestRoads(box, {
roadMode: 'tomtom',
flowSnapshot: Promise.resolve({
hasKey,
segments: hasKey ? [flow] : [],
}),
})
).json();
assert.equal(data.roadSource, hasKey ? 'TomTom' : 'OpenStreetMap');
assert.equal(f.requests(), hasKey ? 0 : 1);
assert.equal(data.roads.length, hasKey ? 1 : 2);
}
});
test('Hybrid streams OSM before flow settles then replaces duplicates with TomTom', async () => {
const f = sourceFixture();
let resolve;
const snapshots = [];
const pending = f.source.requestRoads(box, {
roadMode: 'hybrid',
flowSnapshot: new Promise((done) => {
resolve = done;
}),
onTile: (data) => snapshots.push(data),
});
await Promise.resolve();
assert.equal(snapshots[0].roadSource, 'OpenStreetMap');
assert.equal(snapshots[0].roads.length, 2);
resolve({ hasKey: true, segments: [flow] });
const final = await (await pending).json();
assert.equal(final.roadSource, 'TomTom + OpenStreetMap');
assert.equal(final.roads.length, 2);
assert.ok(snapshots.at(-1).replace);
});
test('road-source params persist in existing layer-state lo field and local storage', () => {
for (const mode of ['tomtom', 'osm', 'hybrid']) {
const state = createDefaultLayerState();
state.enabledLayerIds = ['traffic'];
state.options.traffic.roadMode = mode;
const params = new URLSearchParams('v=2');
encodeLayerStateParams(params, state);
assert.deepEqual([...params.keys()].sort(), ['l', 'lo', 'v']);
assert.equal(decodeLayerStateParams(params).options.traffic.roadMode, mode);
assert.equal(
parseStoredLayerState(serializeStoredLayerState(state)).options.traffic
.roadMode,
mode,
);
}
});
test('row chips use real params; query overrides passive restore and user can change it', () => {
const oldLocation = globalThis.location;
globalThis.location = { search: '?trafficRoads=tomtom' };
try {
const layer = createTrafficLayer({
services: { credits: {}, render: {} },
source: {
requestRoads() {},
getStatus() {},
fetchFlowForBounds() {},
getFlowSessionStats: () => ({}),
resetFlowTileCache() {},
},
});
layer.setParams({ roadMode: 'osm' }, { origin: 'local-restore' });
assert.equal(layer.getParams().roadMode, 'tomtom');
const chips = layer.getRowControls().chips;
assert.equal(chips.length, 3);
assert.equal(chips[0].active, true);
layer.setParams(chips[2].params, { origin: 'user' });
assert.equal(layer.getParams().roadMode, 'hybrid');
assert.match(layer.getStats().loadingLabel, /Hybrid needs a TomTom key/);
assert.equal(layer.getStats().roadMode, 'osm');
} finally {
if (oldLocation === undefined) delete globalThis.location;
else globalThis.location = oldLocation;
}
});
test('Hybrid retains TomTom on OSM outage with a named warning; TomTom failure never invents roads', async () => {
const source = createTrafficSource({
mapTiles: {
fetchBounds: async () => {
throw Object.assign(new Error('refused'), { status: 429 });
},
},
});
const data = await (
await source.requestRoads(box, {
roadMode: 'hybrid',
flowSnapshot: Promise.resolve({ hasKey: true, segments: [flow] }),
})
).json();
assert.equal(data.roads.length, 1);
assert.equal(data.roads[0].directFlow, true);
assert.equal(data.partial, true);
assert.equal(data.roadWarning, 'OpenFreeMap tiles rate-limited');
await assert.rejects(
source.requestRoads(box, {
roadMode: 'tomtom',
flowSnapshot: Promise.resolve({
hasKey: true,
segments: [],
error: 'TomTom daily budget reached',
}),
}),
/TomTom daily budget reached/,
);
});
test('flow application preserves direct values and leaves Hybrid fill simulated', async () => {
const { createFlow } = await import('./flow.js');
const direct = {
coords: coordinates,
directFlow: true,
flow: { level: 0.15, closure: true },
};
const fill = { coords: coordinates, simulatedOnly: true };
const osm = { coords: coordinates };
const state = {
_enabled: true,
_liveMode: true,
_loadGeneration: 1,
_flowPending: 0,
_flowStatusPromise: Promise.resolve(),
};
const controller = createFlow({
state,
services: { credits: {} },
parts: {},
source: { fetchFlowForBounds: async () => [flow] },
});
await controller.applyFlowToRoads([direct, fill, osm], box, 1);
assert.deepEqual(direct.flow, { level: 0.15, closure: true });
assert.equal(fill.flow, null);
assert.deepEqual(osm.flow, { level: 0.3, closure: false });
assert.equal(state._flowPending, 0);
});
test('stale proxy flow tiles reach the layer stats and never read LIVE', async () => {
const { createFlow } = await import('./flow.js');
const { createModel } = await import('./model.js');
const { createControls } = await import('./controls.js');
const { createState } = await import('./state.js');
const { layerFeedState } = await import('../../data/feedState.js');
let detail = {
segments: [flow],
partial: false,
stale: true,
oldestTileAt: 1000,
};
const source = {
fetchFlowDetail: async () => detail,
fetchFlowForBounds: async () => detail.segments,
getFlowSessionStats: () => ({ tilesFetched: 1 }),
};
const state = createState({ services: {} });
Object.assign(state, {
_enabled: true,
_liveMode: true,
_loadGeneration: 1,
_flowStatusPromise: Promise.resolve(),
_bucketCounts: { free: 3, slow: 0, jam: 0, sim: 1 },
});
const parts = {};
const context = { state, services: { credits: {} }, parts, source };
parts.model = createModel(context);
parts.flow = createFlow(context);
const controls = createControls(context);
await parts.flow.applyFlowToRoads([{ coords: coordinates }], box, 1);
const stale = controls.methods.getStats();
assert.equal(stale.mode, 'live');
assert.equal(stale.stale, true);
assert.equal(stale.oldestTileAt, 1000);
assert.doesNotMatch(stale.loadingLabel, /LIVE/);
assert.equal(layerFeedState(stale), 'stale');
// A road failure outranks cached flow: the row reads the failure.
state._roadError = 'Road geometry unavailable';
assert.equal(controls.methods.getStats().stale, false);
state._roadError = null;
detail = {
segments: [flow],
partial: false,
stale: false,
oldestTileAt: 2000,
};
state._loadGeneration = 2;
await parts.flow.applyFlowToRoads([{ coords: coordinates }], box, 2);
const live = controls.methods.getStats();
assert.equal(live.stale, false);
assert.match(live.loadingLabel, /^LIVE · Roads: OpenStreetMap/);
assert.equal(layerFeedState(live), 'nominal');
});
test('status names the drawn road source for each mode', async () => {
const { createModel } = await import('./model.js');
const { trafficFeedPresentation: present } = createModel({
state: {},
services: {},
parts: {},
source: {},
});
assert.equal(
present({ liveMode: true, coveragePct: 100, roadSource: 'TomTom' })
.loadingLabel,
'LIVE · Roads: TomTom · Roads without flow hidden',
);
assert.equal(
present({ liveMode: true, coveragePct: 0, roadSource: 'TomTom' })
.loadingLabel,
'LIVE · Roads: TomTom · No flow roads in view',
);
assert.equal(
present({
liveMode: true,
coveragePct: 81,
roadSource: 'TomTom + OpenStreetMap',
}).loadingLabel,
'LIVE · Roads: TomTom + OpenStreetMap · Flow 81%',
);
assert.match(
present({ liveMode: true, coveragePct: 74 }).loadingLabel,
/^LIVE · Roads: OpenStreetMap · Flow: TomTom · 74% cov/,
);
});
test('only explicit origins clear the query override; restores keep it', () => {
const oldLocation = globalThis.location;
globalThis.location = { search: '?trafficRoads=osm' };
try {
const layer = createTrafficLayer({
services: { credits: {}, render: {} },
source: {
requestRoads() {},
getStatus() {},
fetchFlowForBounds() {},
getFlowSessionStats: () => ({}),
resetFlowTileCache() {},
},
});
for (const origin of ['share-restore', 'local-restore', 'programmatic']) {
layer.setParams({ roadMode: 'tomtom' }, { origin });
assert.equal(layer.getParams().roadMode, 'osm', origin);
}
layer.setParams({ roadMode: 'tomtom' }, { origin: 'voice' });
assert.equal(layer.getParams().roadMode, 'tomtom');
layer.setParams({ roadMode: null }, { origin: 'local-restore' });
assert.equal(layer.getParams().roadMode, null);
} finally {
if (oldLocation === undefined) delete globalThis.location;
else globalThis.location = oldLocation;
}
});
test('TomTom and Hybrid loads skip the OSM snapshot cache; TomTom skips the detail pass', async () => {
const { createIngestion } = await import('./ingestion.js');
for (const [roadMode, roadSource, perLoad] of [
[null, 'TomTom + OpenStreetMap', [true, false]],
['tomtom', 'TomTom', [true]],
]) {
const state = {
_loadGeneration: 0,
_tileCache: new Map(),
_enabled: true,
_liveMode: true,
_roadMode: roadMode,
_parseRoads: (data) => data.roads,
};
const passes = [];
const ingestion = createIngestion({
state,
services: {},
parts: {
viewport: {
clampBounds: (b) => b,
getBoundsCenter: () => ({ lat: 30.267, lon: -97.744 }),
},
flow: {
warmFlow: async () => [flow],
applyFlowThenRender: async () => true,
},
},
source: {
requestRoads: async (_, options) => {
passes.push(options.majorOnly);
assert.equal(options.roadMode, roadMode);
assert.equal((await options.flowSnapshot).hasKey, true);
return {
ok: true,
json: async () => ({ roads: [main], roadSource }),
};
},
},
});
await ingestion.loadRoadsForBounds(box, 350);
await ingestion.loadRoadsForBounds(box, 350);
assert.deepEqual(passes, [...perLoad, ...perLoad], roadSource);
assert.equal(state._tileCache.size, 0);
assert.equal(state._roadSource, roadSource);
}
});
test('a first keyed load whose flow fails never caches simulated fill as OSM roads', async () => {
const { createIngestion } = await import('./ingestion.js');
// Status unknown at the start of the first load; the probe then reports a
// key but flow fails, so Hybrid draws only simulated OpenStreetMap fill,
// labelled OpenStreetMap.
const state = {
_loadGeneration: 0,
_tileCache: new Map(),
_enabled: true,
_liveMode: false,
_roadMode: null,
_parseRoads: (data) => data.roads,
};
const requests = [];
const ingestion = createIngestion({
state,
services: {},
parts: {
viewport: {
clampBounds: (b) => b,
getBoundsCenter: () => ({ lat: 30.267, lon: -97.744 }),
},
flow: {
warmFlow: async () => {
state._liveMode = true;
state._flowStatusKnown = true;
throw Object.assign(new Error('flow'), { status: 502 });
},
deriveTrafficFlowError: () => 'TomTom upstream unreachable',
applyFlowThenRender: async () => true,
},
},
source: {
requestRoads: async (_, options) => {
const live = await options.flowSnapshot;
const mode = resolveRoadMode(options.roadMode, live.hasKey);
requests.push(mode);
const roads =
mode === 'osm' ? [main] : [{ ...main, simulatedOnly: true }];
return {
ok: true,
json: async () => ({
roads,
roadSource: 'OpenStreetMap',
roadMode: mode,
}),
};
},
},
});
await ingestion.loadRoadsForBounds(box, 350);
assert.deepEqual(requests, ['hybrid', 'hybrid']);
assert.equal(state._tileCache.size, 0, 'Hybrid fill is never cached');
// The user switches to OSM: fresh OpenStreetMap roads, not the fill.
state._roadMode = 'osm';
await ingestion.loadRoadsForBounds(box, 350);
assert.deepEqual(requests.slice(2), ['osm', 'osm']);
const [entry] = state._tileCache.values();
assert.ok(entry.full.every((road) => !road.simulatedOnly));
// ...and those plain OSM roads are what a revisit reuses.
await ingestion.loadRoadsForBounds(box, 350);
assert.equal(requests.length, 4);
});
test('a flow snapshot that misses the pass deadline cannot hold roads', async () => {
const never = new Promise(() => {});
const request = (roadMode, hasKey) =>
sourceFixture().source.requestRoads(box, {
roadMode,
flowSnapshot: never,
liveModeHint: () => hasKey,
timeoutSec: 0.02,
});
const keyless = await (await request(null, false)).json();
assert.equal(keyless.roadMode, 'osm');
assert.equal(keyless.roads.length, 2);
const hybrid = await (await request(null, true)).json();
assert.equal(hybrid.roadMode, 'hybrid');
assert.equal(hybrid.roadSource, 'OpenStreetMap');
assert.ok(hybrid.roads.every((road) => road.simulatedOnly));
await assert.rejects(request('tomtom', true), /TomTom flow timed out/);
});
test('frontage roads beside a TomTom motorway survive; same-class duplicates do not', () => {
const offset = (road, metres, extra = {}) => ({
...road,
...extra,
coordinates: road.coordinates.map(([lon, lat]) => [
lon,
lat + metres / 111320,
]),
});
const motorwayLine = { ...flow, roadType: 'Motorway' };
const osmMotorway = { ...main, type: 'motorway' };
const roads = selectTrafficRoads(
[
offset(osmMotorway, 4), // the same motorway: dropped
offset(main, 25, { type: 'residential' }), // frontage, north side
offset(main, -25, { type: 'tertiary' }), // frontage, south side
],
[motorwayLine],
'hybrid',
);
assert.equal(roads.length, 3);
assert.equal(roads[0].directFlow, true);
assert.deepEqual(
roads.slice(1).map((road) => road.type),
['residential', 'tertiary'],
);
// A ramp beside the mainline compares as an ordinary road.
assert.equal(
selectTrafficRoads(
[offset(osmMotorway, 25, { ramp: true })],
[motorwayLine],
'hybrid',
).length,
2,
);
// Across a class mismatch a true duplicate (within 15 m) is still dropped.
assert.equal(
selectTrafficRoads(
[offset(main, 6, { type: 'residential' })],
[motorwayLine],
'hybrid',
).length,
1,
);
// Same class, uncertain 25 m overlap: the OpenStreetMap copy goes.
assert.equal(
selectTrafficRoads(
[offset(main, 25, { type: 'residential' })],
[{ ...flow, roadType: 'Local road' }],
'hybrid',
).length,
1,
);
});
test('private, parking, walking and unknown flow never animate or suppress a public OSM road', () => {
for (const roadType of [
'Non public road',
'Parking road',
'Walkway road',
'',
'Other road',
]) {
const rejected = { ...flow, roadType };
assert.deepEqual(flowSegmentsToRoads([rejected]), []);
const roads = selectTrafficRoads([main], [rejected], 'hybrid');
assert.equal(roads.length, 1);
assert.equal(roads[0].simulatedOnly, true);
assert.deepEqual(roads[0].coordinates, main.coordinates);
}
assert.deepEqual(
flowSegmentsToRoads([{ ...flow, roadCategory: 'walkway' }]),
[],
);
const road = flowSegmentsToRoads([flow])[0];
assert.equal(road.drivable, true);
assert.equal(road.roadClass, 'Major road');
});