refactor: separate traffic and bikeshare components

This commit is contained in:
Sameh Khamis committed 2026-09-13 14:10:38 -07:00
1 parent 3143d00c90
commit 903fe6ba37
45 files changed
+6159 -4363

No files matched your search

+318
View File
@@ -0,0 +1,318 @@
import * as Cesium from 'cesium';
import { flowBucket } from '../../data/trafficFlowStyle.js';
import { trafficStyleProfile } from '../../data/trafficPresetStyle.js';
import {
HEAT_LINE_CAP,
HEAT_JAM_COLOR,
HEAT_SLOW_COLOR,
HEAT_LINE_JAM_WIDTH,
HEAT_JAM_BASE_ALPHA,
HEAT_LINE_SLOW_WIDTH,
TRAFFIC_TIMING_ENABLED,
MAX_DOTS,
} from './policy.js';
export function createRendering({
state: layerState,
services,
parts,
source,
}) {
/**
* At high altitude only major roads render — shared by the render pass and
* the late-flow heat-line rebuild so lines never mark roads without dots.
* @param {Array} roads - Parsed road objects.
* @param {number} altitude - Camera altitude in meters.
* @returns {Array} The roads visible at this altitude.
*/
function visibleRoadsForAltitude(roads, altitude) {
return altitude > 5000
? roads.filter(
(r) =>
r.type === 'motorway' || r.type === 'trunk' || r.type === 'primary',
)
: roads;
}
/** Remove both heat-line ground primitives from the scene. */
function removeHeatLines() {
if (layerState._heatJamPrim) {
layerState._viewer?.scene.groundPrimitives.remove(
layerState._heatJamPrim,
);
layerState._heatJamPrim = null;
}
if (layerState._heatSlowPrim) {
layerState._viewer?.scene.groundPrimitives.remove(
layerState._heatSlowPrim,
);
layerState._heatSlowPrim = null;
}
layerState._heatLineCount = 0;
}
/**
* Rebuild the congestion heat-line underlay (jam-viz heatline prototype):
* slow/jam roads drape a corridor line onto the rendered 3D tiles — glowing
* pulsing red for jam, faint flat amber for slow — with the dots animating
* on top. Two batched GroundPolylinePrimitives (one per bucket) so the jam
* batch pulses through one shared material. Capped at HEAT_LINE_CAP (jam
* first, longest first); overflow is logged. No-op in sim mode, when the
* heatline mode is off, or without ground-primitive support.
*
* @param {Array} roads - Road objects visible in the current render.
*/
function rebuildHeatLines(roads) {
removeHeatLines();
if (
!layerState._viewer ||
!layerState._liveMode ||
!parts.style.heatlineOn()
)
return;
if (layerState._heatSupported === null) {
layerState._heatSupported = Cesium.GroundPolylinePrimitive.isSupported(
layerState._viewer.scene,
);
if (!layerState._heatSupported)
console.warn(
'[Data:Traffic] GroundPolylinePrimitive unsupported — heat-lines disabled',
);
}
if (!layerState._heatSupported) return;
const candidates = [];
for (const road of roads) {
const flow = road.flow;
if (!flow || flow.closure) continue;
const bucket = flowBucket(flow.level);
if (bucket === 'free') continue;
let len = 0;
for (const d of road.segmentDist) len += d;
candidates.push({ road, bucket, len });
}
candidates.sort((a, b) =>
a.bucket === b.bucket ? b.len - a.len : a.bucket === 'jam' ? -1 : 1,
);
const kept = candidates.slice(0, HEAT_LINE_CAP);
const instancesFor = (bucket, width) =>
kept
.filter((c) => c.bucket === bucket)
.map(
(c) =>
new Cesium.GeometryInstance({
geometry: new Cesium.GroundPolylineGeometry({
positions: c.road.waypoints,
width,
}),
}),
);
// Mono presets (NVG/FLIR/noir) discard hue — heat-lines re-encode in
// luminance like the dots: jam = white glow, slow = faint gray.
const monoHeat =
layerState._presetDots === 'on' &&
trafficStyleProfile(layerState._stylePreset) === 'mono';
const jamLineColor = monoHeat ? Cesium.Color.WHITE : HEAT_JAM_COLOR;
const slowLineColor = monoHeat
? new Cesium.Color(0.7, 0.7, 0.7, HEAT_SLOW_COLOR.alpha)
: HEAT_SLOW_COLOR;
const jamInstances = instancesFor('jam', HEAT_LINE_JAM_WIDTH);
if (jamInstances.length) {
layerState._heatJamPrim = layerState._viewer.scene.groundPrimitives.add(
new Cesium.GroundPolylinePrimitive({
geometryInstances: jamInstances,
classificationType: Cesium.ClassificationType.CESIUM_3D_TILE,
appearance: new Cesium.PolylineMaterialAppearance({
material: Cesium.Material.fromType('PolylineGlow', {
color: jamLineColor.withAlpha(HEAT_JAM_BASE_ALPHA),
glowPower: 0.25,
}),
}),
}),
);
}
const slowInstances = instancesFor('slow', HEAT_LINE_SLOW_WIDTH);
if (slowInstances.length) {
layerState._heatSlowPrim = layerState._viewer.scene.groundPrimitives.add(
new Cesium.GroundPolylinePrimitive({
geometryInstances: slowInstances,
classificationType: Cesium.ClassificationType.CESIUM_3D_TILE,
appearance: new Cesium.PolylineMaterialAppearance({
material: Cesium.Material.fromType('Color', {
color: slowLineColor,
}),
}),
}),
);
}
layerState._heatLineCount = kept.length;
if (candidates.length > kept.length) {
console.log(
`[Data:Traffic] Heat-lines capped at ${HEAT_LINE_CAP} (${candidates.length} congested roads in view)`,
);
}
}
/**
* Clear existing dots and re-spawn them for the given road set and altitude.
*
* When zoomed out (>5 km), only major road types are rendered to reduce clutter.
* Dot budgets are allocated fairly across visible roads via `allocateRoadDotBudgets`.
*
* @param {Array} roads - Parsed road objects to render.
* @param {number} altitude - Camera altitude in meters.
* @param {string} label - Logging label (e.g. "Cache full", "Loaded major").
* @param {Object|null} [trace=null] - Development-only correlated load trace.
*/
function renderRoadsForAltitude(roads, altitude, label, trace = null) {
const state =
TRAFFIC_TIMING_ENABLED && trace
? parts.timing.trafficTimingRenderState(trace, label)
: null;
const renderId = state ? ++trace.renderSequence : null;
parts.animation.clearDots();
layerState._roads = roads;
layerState._lastRenderAltitude = altitude;
// At high altitude, drop minor roads to reduce visual noise
const filteredRoads = visibleRoadsForAltitude(roads, altitude);
// Closed roads spawn zero dots (computeDotCount/spawnDotsForRoad) — count
// them here so the closure signal is visible in stats even at zero dots.
layerState._closedRoads = layerState._liveMode
? filteredRoads.reduce((n, r) => n + (r.flow?.closure ? 1 : 0), 0)
: 0;
// Fade distances must track the camera-to-AREA distance, not assume a
// nadir view: oblique pitches put the loaded roads many km away even at
// low altitude. Probe three roads and stretch the curves accordingly.
let areaDist = altitude;
if (layerState._viewer && filteredRoads.length) {
const probes = [
filteredRoads[0],
filteredRoads[Math.floor(filteredRoads.length / 2)],
filteredRoads[filteredRoads.length - 1],
];
for (const probe of probes) {
const wp = probe?.waypoints?.[0];
if (wp)
areaDist = Math.max(
areaDist,
Cesium.Cartesian3.distance(
layerState._viewer.camera.positionWC,
wp,
),
);
}
}
layerState._fadeScaleFar = Math.max(8000, areaDist * 1.5);
layerState._fadeTransFar = Math.max(10000, areaDist * 1.8);
const dotStart = state
? parts.timing.trafficTimingMark(state, 'dot-construction-start', {
renderId,
renderLabel: label,
roadCount: roads.length,
visibleRoadCount: filteredRoads.length,
})
: null;
const roadBudgets = parts.model.allocateRoadDotBudgets(
filteredRoads,
altitude,
MAX_DOTS,
);
for (let i = 0; i < filteredRoads.length; i++) {
const road = filteredRoads[i];
const budget = roadBudgets[i] || 0;
if (budget <= 0) continue;
parts.animation.spawnDotsForRoad(road, altitude, budget);
if (layerState._dots.length >= MAX_DOTS) break;
}
const renderMetrics = state
? {
renderId,
renderLabel: label,
roadCount: roads.length,
visibleRoadCount: filteredRoads.length,
dotCount: layerState._dots.length,
}
: null;
if (state) {
const dotEnd = parts.timing.trafficTimingMark(
state,
'dot-construction-end',
renderMetrics,
);
parts.timing.trafficTimingMeasure(
'dot-construction',
state,
dotStart,
dotEnd,
renderMetrics,
);
}
const heatStart = state
? parts.timing.trafficTimingMark(
state,
'rebuild-heat-lines-start',
renderMetrics,
)
: null;
rebuildHeatLines(filteredRoads);
if (state) {
const heatEnd = parts.timing.trafficTimingMark(
state,
'rebuild-heat-lines-end',
{
...renderMetrics,
heatLineCount: layerState._heatLineCount,
},
);
parts.timing.trafficTimingMeasure(
'rebuild-heat-lines',
state,
heatStart,
heatEnd,
{
...renderMetrics,
heatLineCount: layerState._heatLineCount,
},
);
}
layerState._count = layerState._dots.length;
layerState._lastUpdate = Date.now();
console.log(
`[Data:Traffic] ${label}: ${layerState._count} dots (roads=${roads.length}, alt=${Math.round(altitude)}m)`,
);
if (state) {
const renderEnd = parts.timing.trafficTimingMark(
state,
'render-return',
renderMetrics,
);
parts.timing.scheduleTrafficTimingPostRender(
state,
renderEnd,
renderId,
renderMetrics,
);
}
}
return {
visibleRoadsForAltitude,
removeHeatLines,
rebuildHeatLines,
renderRoadsForAltitude,
};
}