mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 23:32:20 +00:00
Merge pull request #1734 from bilawalsidhu/transit/route-type-models
Draw transit vehicles as trolleybuses, cable cars, funiculars, gondolas and monorails
This commit is contained in:
32 files changed
+717
-60
No files matched your search
@@ -0,0 +1,10 @@
|
||||
- Draw transit vehicles as their kind, not one of five: trolleybuses, cable
|
||||
cars, funiculars, gondolas and monorails are modes of their own, with their
|
||||
own icon, colour, card word and fleet model. A vehicle record may carry its
|
||||
route's GTFS `route_type` (`routeType`), which settles its mode before the
|
||||
feed's route hints and picks the finer model (a coach for 200, a high-speed
|
||||
set for 101). Prague's trolleybuses and Petřín funicular, and Bucharest,
|
||||
Cluj and Turin's trams, trolleybuses and metro lines, now read as such from
|
||||
the route ids in each operator's routes.txt. Le Havre's funicular, which its
|
||||
routes.txt types as a bus, is a named correction that outranks the route
|
||||
type (samehkhamis, #1734).
|
||||
+25
-5
@@ -5868,7 +5868,26 @@ Helsinki, the Netherlands, Norway, South East Queensland, Rio de Janeiro
|
||||
Ontario — Toronto (TTC surface vehicles; the subway publishes no
|
||||
positions), MiWay, Hamilton HSR, Durham Region, YRT/Viva, Guelph,
|
||||
Burlington, Barrie and Kingston. Mode silhouettes
|
||||
identify buses, trams, subways, trains, ferries and unclassified vehicles.
|
||||
identify buses, trolleybuses, trams, cable cars, subways, monorails, trains,
|
||||
funiculars, gondolas (aerial lifts), ferries and unclassified vehicles.
|
||||
|
||||
A vehicle's mode comes from, in order: the feed's fleet-number hint
|
||||
(`vehicleMode`, which knows a bus standing in on a streetcar route); the feed's
|
||||
correction for a route its own routes.txt mistypes (`routeCorrections`: Le
|
||||
Havre's funicular `8-FUNI`, which LiA's routes.txt types as a bus); the GTFS
|
||||
`route_type` of its route, when its record carries one as `routeType` (basic
|
||||
0–12 or extended 100–1799, each extended type taken as the basic mode it is a
|
||||
kind of: 200 coach is a bus, 101 high-speed is rail, 405 is a monorail); the
|
||||
feed's route-id hint (`routeMode`); and the feed's default (`transitModeFor`,
|
||||
`src/data/transitFeeds.js`). The GTFS-Realtime decoder carries no route type, so
|
||||
a record from the bundled proxy is classified by the hints alone. Route hints
|
||||
read from each operator's routes.txt cover Prague's trolleybus lines and Petřín
|
||||
funicular, and Bucharest, Cluj-Napoca and Turin's trams, trolleybuses and metro
|
||||
lines (and Turin's Sassi–Superga rack tramway, typed a funicular in its
|
||||
routes.txt). Le Havre's funicular is a correction, not read from routes.txt: its
|
||||
route hint and its correction both say funicular. Vilnius publishes no route ids in its positions, so
|
||||
its trolleybuses read as buses. Retained history classifies its epochs the same
|
||||
way, so a trail joins the live vehicle.
|
||||
|
||||
Retained transit history uses the same outlier quarantine as live observations and
|
||||
preserves the active live bracket. Rejected reports do not publish route or mode
|
||||
@@ -5934,9 +5953,10 @@ External halos are 1/1.5 px in normal, 1.25/1.25 px in CRT and 2/2 px in sensors
|
||||
final unpadded display size. Each mode has an outline with a front (a nose, a
|
||||
bow or a chamfered cab) for a vehicle with a known course and a symmetric
|
||||
outline for one without, which would otherwise sit at rotation 0 and point
|
||||
north; a vehicle switches to the pointed glyph when a course arrives. Six modes, two sizes, three
|
||||
profiles and the two outlines cap the raster cache at 72 variants. Normal mode colours are bus `#5EF08A`, tram
|
||||
`#FFC24A`, subway `#FF4538`, rail `#D9A6FF`, ferry `#5FD6FF`, and unknown
|
||||
north; a vehicle switches to the pointed glyph when a course arrives. Eleven modes, two sizes, three
|
||||
profiles and the two outlines cap the raster cache at 132 variants. Normal mode colours are bus `#5EF08A`,
|
||||
trolleybus `#B6F04A`, tram `#FFC24A`, cable car `#FF8F4A`, subway `#FF4538`, monorail `#FF6FB8`, rail
|
||||
`#D9A6FF`, funicular `#E8C98A`, gondola `#7FA8FF`, ferry `#5FD6FF`, and unknown
|
||||
`#D8DDE5`. Selection keeps the mode colour. Sensors reuse each normal silhouette's exact
|
||||
outer path, filled opaque white without interior panel lines, with an opaque
|
||||
`#05080C` halo. Only the external padding grows; the unpadded silhouette remains
|
||||
@@ -7430,7 +7450,7 @@ pins and share links already supply complete boxes.
|
||||
30-second backoff ahead of the current clock are ignored. A smaller rollback
|
||||
can retain a bounded retry delay of up to 30 seconds.
|
||||
- Commercial and military aircraft use the same high-level FLEET model regime: 2D billboards when zoomed out, optional glTF models when closer, controlled by the DISPLAY rail `3D` toggle and `Proximity` / `All` modes. Every model is one of the project's procedural fleet models (`public/models/fleet/`, MIT, 122 GLBs, about 2 MB in all; `public/models/README.md`), chosen per aircraft by `aircraftModelSpec` (`src/data/fleetModels.js`): the first of its ICAO type's callsign rules in `types.json` (`aircraft_callsign`) its callsign matches, else its type's entry, else the model of its `classifyAircraft` class for a type the map lacks, else its ADS-B emitter category's, else the default airliner. It is drawn LIFE SIZE: the type's published length over the model's built length (`src/layers/flights/modelSpec.js`, shared by Flights, Military Flights and Local ADS-B), and a grounded aircraft stands the model's scaled clearance above the ground. An airport ground vehicle (feed type `ZVEH`) is a fire truck (`FIRE…`), pushback tug (`PUSH…`, `TUG…`), van (`DEICER…`, `SQUID…`, `SQB…`) or else a car by its callsign, and a ground station (`GRND`) is an 11.8 m radio mast; these models (catalog category `other`) are drawn at their built size, and such a contact with no altitude stands on the ground instead of at the airborne default (`src/layers/flights/records.js`). `classifyAircraft` still picks the 2D icons. The whole set loads once, up front, when a layer that draws it starts (`loadFleetModelSet`, which logs `[Fleet] N/122 models (X MB) loaded in N ms`); models are built from those bytes, never fetched mid-session, and an aircraft stays its billboard until the set has loaded. Every aircraft model draws its mesh's own colours (`aircraftModelBlend`, `src/data/aircraftModelLighting.js`): Cesium's `ColorBlendMode.HIGHLIGHT` with white, in Flights, Military Flights and Local ADS-B alike; the tracked colour (cyan, or amber in Military Flights) and the emergency red multiply into the mesh, so its paint still shows. IR boost draws them flat white (`MIX` at `1.0`). Under NVG/FLIR (map preset or Cockpit vision) models render unlit flat-white at full alpha and scene fog is disabled (fog otherwise blacks out distant models with the globe hidden); state restores on exit.
|
||||
- Vessels, Transit and Rail draw fleet models under the same DISPLAY rail `3D` toggle, at the same altitude ceiling, caps and add/keep radii as the aircraft (`FLEET_MODEL_POLICY`, `src/layers/flights/policy.js`; `createFleetModelLayer`, `src/data/fleetModelInstances.js`). A vessel's model follows its AIS ship type and is drawn at its reported length and beam (AIS static dimensions A+B and C+D, now carried on vessel rows as `length` and `beam`), standing at the sea surface, heading by true heading then course. An aid to navigation is not a hull: a vessel record whose type names a buoy, beacon, mark, RACON or light, or whose MMSI is an aid-to-navigation station's (`99MIDxxxx`), is `isAidToNavigation` (`src/data/vesselTypes.js`) and is drawn as the set's `buoys` model of the IALA mark its type names (`aidToNavigationModel`, `src/data/fleetModels.js`: cardinal N/E/S/W, port or starboard hand, preferred channel by its side, safe water, isolated danger, special mark; a beacon, light, light vessel, RACON or reference point with no mark of its own is the special mark), else a red can (`DEFAULT_AID_MODEL`), at its built size and facing north whatever heading it reports. A type is an aid's when it is one of the 32 kinds an aid report names or uses a word for one (buoy, beacon, cardinal, safe water, RACON, lighthouse and so on); a vessel that works on aids (`Buoy Tender`, `Buoy Layer`) is not one. Its icon is a bone-white ringed disc (`AID_TO_NAVIGATION_STYLE`, `src/data/vesselLabels.js`) instead of a chevron, never rotated, and its card, selected card and HUD readout (`src/layers/vessels/cards.js`) show its kind alone, whole, in that colour: no speed or heading, since a fixed mark has neither. AISStream subscribes to `AidsToNavigationReport` (message 21) by default, and the server names each report's aid type code (0 to 31) as its kind (`aidToNavigationTypeName`: `20` is `Cardinal Mark N`) rather than reading it as a ship type, and its name runs on into the report's `NameExtension`. A virtual aid (the report's `VirtualAtoN` flag: a mark broadcast where nothing physical stands) carries `virtual: true` on its row, and on its vessel source record (the live-source contract's `virtual`, `src/sources/live/contract.js`, false unless a source says true; any source may set it). It keeps its disc but never gets a model (`vesselModelSpec` returns null for it, and the vessel layer gathers no model item for it), and its card and readout add `VIRTUAL` after its kind. `aisStreamRows` (`server/providers/vessels/ais-store.js`) puts every vessel ahead of every aid, so a capped request (the layer asks for 12,000 rows) never gives up a ship for a buoy. The `vessels_in_area` tool (and the brief built on it) counts ships only: it leaves aids out of its rows, counts, breakdowns and speed stats. A transit vehicle's model follows its layer mode (bus, tram, subway, rail or ferry; a vehicle of `unknown` mode keeps its icon), a train's its rail category and mode (rail, metro or tram). Neither the transit nor the rail sources carry a GTFS `route_type` or a finer mode (monorail, gondola, funicular, cable car), so the set's models for those and their `transit.elevation` lifts are not reached by any live source today. Both stand on the road or rail top they are drawn on, without the 1.5 m `GROUND_FLOOR_LIFT_M` their icons carry, and are painted in their mode or category colour where the model's `_TINT` attribute marks it. A vessel model is clipped at its waterline (one clipping plane per model, at the model origin's horizontal plane), so its hull below the water never shows through a plain globe, which Cesium does not depth-test models against. Under the NVG/FLIR styles (the Flights `irBoost` parameter) these models render unlit flat white, as aircraft models do. Each vehicle's icon stays the visual until its model is loaded, ready and placed; a model is picked as its icon is, and a vehicle drawn as its model keeps its selection card and its place in the detection overlay.
|
||||
- Vessels, Transit and Rail draw fleet models under the same DISPLAY rail `3D` toggle, at the same altitude ceiling, caps and add/keep radii as the aircraft (`FLEET_MODEL_POLICY`, `src/layers/flights/policy.js`; `createFleetModelLayer`, `src/data/fleetModelInstances.js`). A vessel's model follows its AIS ship type and is drawn at its reported length and beam (AIS static dimensions A+B and C+D, now carried on vessel rows as `length` and `beam`), standing at the sea surface, heading by true heading then course. An aid to navigation is not a hull: a vessel record whose type names a buoy, beacon, mark, RACON or light, or whose MMSI is an aid-to-navigation station's (`99MIDxxxx`), is `isAidToNavigation` (`src/data/vesselTypes.js`) and is drawn as the set's `buoys` model of the IALA mark its type names (`aidToNavigationModel`, `src/data/fleetModels.js`: cardinal N/E/S/W, port or starboard hand, preferred channel by its side, safe water, isolated danger, special mark; a beacon, light, light vessel, RACON or reference point with no mark of its own is the special mark), else a red can (`DEFAULT_AID_MODEL`), at its built size and facing north whatever heading it reports. A type is an aid's when it is one of the 32 kinds an aid report names or uses a word for one (buoy, beacon, cardinal, safe water, RACON, lighthouse and so on); a vessel that works on aids (`Buoy Tender`, `Buoy Layer`) is not one. Its icon is a bone-white ringed disc (`AID_TO_NAVIGATION_STYLE`, `src/data/vesselLabels.js`) instead of a chevron, never rotated, and its card, selected card and HUD readout (`src/layers/vessels/cards.js`) show its kind alone, whole, in that colour: no speed or heading, since a fixed mark has neither. AISStream subscribes to `AidsToNavigationReport` (message 21) by default, and the server names each report's aid type code (0 to 31) as its kind (`aidToNavigationTypeName`: `20` is `Cardinal Mark N`) rather than reading it as a ship type, and its name runs on into the report's `NameExtension`. A virtual aid (the report's `VirtualAtoN` flag: a mark broadcast where nothing physical stands) carries `virtual: true` on its row, and on its vessel source record (the live-source contract's `virtual`, `src/sources/live/contract.js`, false unless a source says true; any source may set it). It keeps its disc but never gets a model (`vesselModelSpec` returns null for it, and the vessel layer gathers no model item for it), and its card and readout add `VIRTUAL` after its kind. `aisStreamRows` (`server/providers/vessels/ais-store.js`) puts every vessel ahead of every aid, so a capped request (the layer asks for 12,000 rows) never gives up a ship for a buoy. The `vessels_in_area` tool (and the brief built on it) counts ships only: it leaves aids out of its rows, counts, breakdowns and speed stats. A transit vehicle's model follows its route's GTFS `route_type` when its record carries one and that type is a kind of the vehicle's mode (`types.json` `transit.route_types`: 200 a coach, 101 a high-speed set, 106 a regional DMU, 710 a double-decker), else its layer mode (`transit.modes`: bus, trolleybus, tram, cable car, subway, monorail, rail, funicular, gondola or ferry; a vehicle of `unknown` mode keeps its icon); a fleet hint that overrules the route (a bus on a tram route) leaves the type unused. A monorail and a gondola are raised by their `transit.elevation` lifts. A train's model follows its rail category and mode (rail, metro or tram). The bundled GTFS-Realtime proxy carries no route type, so its vehicles draw their mode's model; a source that adds `routeType` reaches the finer ones. Both stand on the road or rail top they are drawn on, without the 1.5 m `GROUND_FLOOR_LIFT_M` their icons carry, and are painted in their mode or category colour where the model's `_TINT` attribute marks it. A vessel model is clipped at its waterline (one clipping plane per model, at the model origin's horizontal plane), so its hull below the water never shows through a plain globe, which Cesium does not depth-test models against. Under the NVG/FLIR styles (the Flights `irBoost` parameter) these models render unlit flat white, as aircraft models do. Each vehicle's icon stays the visual until its model is loaded, ready and placed; a model is picked as its icon is, and a vehicle drawn as its model keeps its selection card and its place in the detection overlay.
|
||||
- **Aircraft models skip Cesium's dynamic environment map (2026-10-24).** Flights, Military Flights and Local ADS-B pass `environmentMapOptions: AIRCRAFT_ENVIRONMENT_MAP_OPTIONS` (`{ enabled: false }`, `src/data/aircraftModelLighting.js`) to every drawn model. Otherwise each `Model` renders the sky around it and reads it back from the GPU (`readPixels`) when it is first placed, and again whenever it moves more than 1 km, the scene's atmosphere settings change, or (under sunlight dynamic lighting) an hour of scene time passes (`DynamicEnvironmentMapManager`, Cesium 1.138). The models are lit from Cesium's default spherical harmonics instead, with no specular environment map, so the sky's model-lighting cube map no longer reaches aircraft. `src/data/aircraftModelLighting.test.mjs` pins the option at every drawn model call; every model is built from the fleet set loaded up front, so there is no separate preload.
|
||||
- The DISPLAY-rail `3D` toggle is the user-facing activation path for both aircraft layers. Their small approved GLBs build their render resources without Cesium's frame-spread job queue, preventing continuous Photorealistic 3D Tiles streaming from starving model readiness; model caps, tracking, camera, and fallback billboards are unchanged.
|
||||
- **The `3D` toggle DEFAULTS ON in `proximity` on a first run.** Proximity is itself the budget — models appear only below `MODEL_ALT_CEIL_M` and only for the nearest `MODEL_MAX` in view — so the default costs nothing at globe scale, and `all` remains a deliberate opt-in. A fresh boot runs NO layer-state restoration (`LayerStateCoordinator.start()` returns early with neither a share payload nor stored state), so four independent initializers decide what a first-run operator sees and must agree: `booleanOption('models3d', 'e', true)` in `src/data/layerState.js`, `_models3dEnabled = true` in BOTH flight layers, `this._models3dEnabled = true` in `src/ui/aircraftDisplay.js`, and the `active` / `visible` classes on `#models3d-toggle` / `#models3d-mode-row` in `index.html`. All four are pinned together in `src/data/layerState.test.mjs`. Explicit state still wins: because the codec omits default-valued options, `models3d: false` is now what travels in a link (`lo=…f.e.0`) and restores OFF at both aircraft layers. **Consequence for returning users:** a stored `gev:layer-state:v2` blob is a FULL options snapshot, so a session that wrote one before this change carries `models3d:false` and keeps 3D off until the operator flips it (or clears the key) — the durable snapshot is treated as the recipient's own state, by design.
|
||||
|
||||
@@ -202,3 +202,25 @@ test('a feed fleet-number mode outranks its route mode in the epoch context', (t
|
||||
assert.equal(h.get('ttc', '4507', 100000).epochs[0].mode, 'tram');
|
||||
assert.equal(h.get('ttc', 'x1', 100000).epochs[0].mode, 'tram');
|
||||
});
|
||||
test('the epoch mode is the live mode: a route type, else hints, else the default', (t) => {
|
||||
const h = history(t);
|
||||
const pid = {
|
||||
id: 'pid',
|
||||
historyRetention: true,
|
||||
defaultMode: 'bus',
|
||||
routeMode: (route) => (route === 'L22' ? 'tram' : 'unknown'),
|
||||
};
|
||||
h.ingest(
|
||||
pid,
|
||||
[
|
||||
row(100000, 'a', { routeId: 'L58', routeType: 11 }),
|
||||
row(100000, 'b', { routeId: 'L22' }),
|
||||
row(100000, 'c', { routeId: 'L9999' }),
|
||||
],
|
||||
100000,
|
||||
);
|
||||
assert.equal(h.get('pid', 'a', 100000).epochs[0].mode, 'trolleybus');
|
||||
assert.equal(h.get('pid', 'b', 100000).epochs[0].mode, 'tram');
|
||||
// A hint that knows nothing leaves the default, as the live layer does.
|
||||
assert.equal(h.get('pid', 'c', 100000).epochs[0].mode, 'bus');
|
||||
});
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
vesselModelSpec,
|
||||
} from './fleetModels.js';
|
||||
import { FLEET_TRAIL_ANCHORS } from './fleetTrailAnchors.js';
|
||||
import { TRANSIT_MODES } from './transitFeeds.js';
|
||||
import {
|
||||
centrelineSegments,
|
||||
distanceToSurface,
|
||||
@@ -444,6 +445,38 @@ test('aids to navigation are the buoy of the mark their type names', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('every transit mode but unknown draws its own model, and a route type a finer one', () => {
|
||||
for (const mode of TRANSIT_MODES.filter((m) => m !== 'unknown')) {
|
||||
assert.ok(types.transit.modes[mode], `${mode} has a model`);
|
||||
assert.equal(
|
||||
resolveTransit(types, { mode }).model,
|
||||
types.transit.modes[mode][0],
|
||||
);
|
||||
}
|
||||
for (const [mode, model] of [
|
||||
['trolleybus', 'trolleybus'],
|
||||
['cable_tram', 'cable_tram'],
|
||||
['aerial_lift', 'gondola'],
|
||||
['funicular', 'funicular'],
|
||||
['monorail', 'monorail'],
|
||||
])
|
||||
assert.equal(resolveTransit(types, { mode }).model, model, mode);
|
||||
// A route type outranks the mode: a coach and a high-speed set are still
|
||||
// the bus and rail modes.
|
||||
for (const [routeType, mode, model] of [
|
||||
[200, 'bus', 'coach'],
|
||||
[101, 'rail', 'high_speed'],
|
||||
[106, 'rail', 'regional_dmu'],
|
||||
[710, 'bus', 'double_decker'],
|
||||
])
|
||||
assert.equal(resolveTransit(types, { routeType, mode }).model, model);
|
||||
// A type the table lacks falls back to the mode.
|
||||
assert.equal(
|
||||
resolveTransit(types, { routeType: 300, mode: 'rail' }).model,
|
||||
'commuter_emu',
|
||||
);
|
||||
});
|
||||
|
||||
test('transit specs: rail category and mode, then route type, then mode, then the default', () => {
|
||||
assert.deepEqual(resolveTransit(types, { railCategory: 'national' }), {
|
||||
model: 'intercity',
|
||||
|
||||
+239
-16
@@ -29,12 +29,21 @@ import { RIO_ROUTE_NAMES } from './rioRouteNames.js';
|
||||
* node:test. No Cesium, no Node built-ins.
|
||||
*/
|
||||
|
||||
/** Transit modes the layer colors. `routeMode` hints refine a feed's default. */
|
||||
/**
|
||||
* Transit modes the layer colors: the GTFS basic route types, by name. A
|
||||
* vehicle's GTFS `route_type` settles its mode when its record carries one;
|
||||
* otherwise `routeMode` hints refine a feed's default.
|
||||
*/
|
||||
export const TRANSIT_MODES = Object.freeze([
|
||||
'bus',
|
||||
'trolleybus',
|
||||
'tram',
|
||||
'cable_tram',
|
||||
'subway',
|
||||
'monorail',
|
||||
'rail',
|
||||
'funicular',
|
||||
'aerial_lift',
|
||||
'ferry',
|
||||
'unknown',
|
||||
]);
|
||||
@@ -42,13 +51,81 @@ export const TRANSIT_MODES = Object.freeze([
|
||||
/** Per-mode icon used in labels and the selection card. */
|
||||
export const TRANSIT_MODE_ICON = Object.freeze({
|
||||
bus: '🚌',
|
||||
trolleybus: '🚎',
|
||||
tram: '🚊',
|
||||
cable_tram: '🚋',
|
||||
subway: '🚇',
|
||||
monorail: '🚝',
|
||||
rail: '🚆',
|
||||
funicular: '🚞',
|
||||
aerial_lift: '🚡',
|
||||
ferry: '⛴️',
|
||||
unknown: '🚏',
|
||||
});
|
||||
|
||||
/** The basic GTFS route types (0–7, 11, 12) by mode. */
|
||||
const BASIC_ROUTE_TYPE_MODES = Object.freeze({
|
||||
0: 'tram',
|
||||
1: 'subway',
|
||||
2: 'rail',
|
||||
3: 'bus',
|
||||
4: 'ferry',
|
||||
5: 'cable_tram',
|
||||
6: 'aerial_lift',
|
||||
7: 'funicular',
|
||||
11: 'trolleybus',
|
||||
12: 'monorail',
|
||||
});
|
||||
|
||||
/**
|
||||
* A GTFS `route_type` as an integer: a basic type (0–12) or an extended one
|
||||
* (100–1799), as a number or its decimal text. Null for anything else.
|
||||
* @param {unknown} value
|
||||
* @returns {number|null}
|
||||
*/
|
||||
export function transitRouteType(value) {
|
||||
const n =
|
||||
typeof value === 'number'
|
||||
? value
|
||||
: typeof value === 'string' && /^\d{1,4}$/.test(value.trim())
|
||||
? Number(value.trim())
|
||||
: NaN;
|
||||
return Number.isInteger(n) && n >= 0 && n < 1800 ? n : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The mode of a GTFS `route_type`: the basic types, and the extended ones
|
||||
* mapped to the basic mode they are a kind of (a coach, 200, is a bus; a
|
||||
* high-speed train, 101, is rail; 405 is a monorail). Air, taxi and other
|
||||
* services, and a value GTFS does not define, are `unknown`.
|
||||
* @param {unknown} routeType
|
||||
* @returns {string} One of TRANSIT_MODES.
|
||||
*/
|
||||
export function transitModeOfRouteType(routeType) {
|
||||
const n = transitRouteType(routeType);
|
||||
if (n === null) return 'unknown';
|
||||
if (n < 100) return BASIC_ROUTE_TYPE_MODES[n] ?? 'unknown';
|
||||
if (n === 405) return 'monorail';
|
||||
const family = Math.floor(n / 100);
|
||||
return (
|
||||
{
|
||||
1: 'rail', // railway
|
||||
2: 'bus', // coach
|
||||
3: 'rail', // suburban railway
|
||||
4: 'subway', // urban railway
|
||||
5: 'subway', // metro
|
||||
6: 'subway', // underground
|
||||
7: 'bus',
|
||||
8: 'trolleybus',
|
||||
9: 'tram',
|
||||
10: 'ferry', // water transport
|
||||
12: 'ferry', // ferry
|
||||
13: 'aerial_lift',
|
||||
14: 'funicular',
|
||||
}[family] ?? 'unknown'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* MBTA route ids are human-readable and mode-typed: rapid-transit lines carry
|
||||
* colour names, commuter rail is prefixed `CR-`, ferries `Boat-`, buses are
|
||||
@@ -443,7 +520,7 @@ const liaRouteLabel = routeIdPattern(/^8-0*(\w.*)$/);
|
||||
function liaRouteMode(routeId) {
|
||||
if (!routeId) return 'unknown';
|
||||
const line = liaRouteLabel(routeId);
|
||||
if (line === 'FUNI') return 'rail';
|
||||
if (line === 'FUNI') return 'funicular';
|
||||
return /^[AB]$/.test(line) ? 'tram' : 'bus';
|
||||
}
|
||||
|
||||
@@ -470,10 +547,14 @@ function astuceRouteLabel(routeId) {
|
||||
return ASTUCE_ROUTES[routeId ?? '']?.label ?? astuceNetworkLabel(routeId);
|
||||
}
|
||||
|
||||
/** PID trolleybus lines: route_type 11 in PID's routes.txt (checked 2026-11-14). */
|
||||
const PID_TROLLEYBUS_LINES = new Set([51, 52, 53, 58, 59, 232, 275]);
|
||||
|
||||
/**
|
||||
* Prague PID route ids are `L` plus a number whose range says what runs it,
|
||||
* per PID's GTFS routes.txt (data.pid.cz, checked 2026-11-09): 1–99 trams
|
||||
* (50–59 trolleybuses, 49 the Petřín funicular `LD`), 100–990 buses (the
|
||||
* trolleybus routes 232 and 275 among them, GTFS route type 11; the
|
||||
* 800s are the X replacement lines, `L803` is X3), 991–993 metro A–C,
|
||||
* 1000s and 1100s the S and R trains (`L1004` is S4, `L1109` R9), the rest
|
||||
* of 1200–1399 other trains, 1700s town buses, 1800s the P ferries, and from
|
||||
@@ -487,7 +568,9 @@ function pidRoute(routeId) {
|
||||
if (!match) return { mode: 'unknown', label: routeId };
|
||||
const n = Number(match[1]);
|
||||
// i18n-ignore: line code, as signed
|
||||
if (n === 49) return { mode: 'rail', label: 'LD' };
|
||||
if (n === 49) return { mode: 'funicular', label: 'LD' };
|
||||
if (PID_TROLLEYBUS_LINES.has(n))
|
||||
return { mode: 'trolleybus', label: String(n) };
|
||||
if (n >= 50 && n <= 59) return { mode: 'bus', label: String(n) };
|
||||
if (n < 100) return { mode: 'tram', label: String(n) };
|
||||
if (n >= 800 && n < 900) return { mode: 'bus', label: `X${n - 800}` };
|
||||
@@ -512,6 +595,102 @@ function pidRouteLabel(routeId) {
|
||||
return pidRoute(routeId).label;
|
||||
}
|
||||
|
||||
/**
|
||||
* A feed's route hint from its static GTFS routes.txt: the route ids of
|
||||
* every mode but `bus`, which is everything else.
|
||||
* @param {Record<string, string[]>} idsByMode
|
||||
* @returns {(routeId: string|null) => string}
|
||||
*/
|
||||
function routeModeTable(idsByMode) {
|
||||
const modes = new Map(
|
||||
Object.entries(idsByMode).flatMap(([mode, ids]) =>
|
||||
ids.map((id) => [id, mode]),
|
||||
),
|
||||
);
|
||||
return (routeId) => (routeId ? (modes.get(routeId) ?? 'bus') : 'unknown');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bucharest TPBI by route id, from its routes.txt (checked 2026-11-14): the
|
||||
* trams (route type 0), metro lines M1–M5 (1) and trolleybuses (11); every
|
||||
* other route is a bus. Realtime route ids are the static ones.
|
||||
*/
|
||||
const tpbiRouteMode = routeModeTable({
|
||||
tram: [
|
||||
'54',
|
||||
'55',
|
||||
'57',
|
||||
'61',
|
||||
'63',
|
||||
'65',
|
||||
'66',
|
||||
'67',
|
||||
'68',
|
||||
'69',
|
||||
'70',
|
||||
'814',
|
||||
'915',
|
||||
'1013',
|
||||
],
|
||||
subway: ['968', '969', '970', '971', '999'],
|
||||
trolleybus: [
|
||||
'72',
|
||||
'73',
|
||||
'74',
|
||||
'75',
|
||||
'76',
|
||||
'78',
|
||||
'81',
|
||||
'83',
|
||||
'84',
|
||||
'85',
|
||||
'86',
|
||||
'87',
|
||||
'247',
|
||||
'910',
|
||||
'916',
|
||||
'924',
|
||||
'925',
|
||||
],
|
||||
});
|
||||
|
||||
/**
|
||||
* Cluj-Napoca CTP by route id, from its routes.txt (checked 2026-11-14): the
|
||||
* trams 100–102L (route type 0) and the trolleybuses (11).
|
||||
*/
|
||||
const ctpClujRouteMode = routeModeTable({
|
||||
tram: ['2', '3', '4', '5'],
|
||||
trolleybus: [
|
||||
'1',
|
||||
'10',
|
||||
'14',
|
||||
'15',
|
||||
'22',
|
||||
'36',
|
||||
'49',
|
||||
'53',
|
||||
'54',
|
||||
'55',
|
||||
'117',
|
||||
'119',
|
||||
'120',
|
||||
'153',
|
||||
'166',
|
||||
'187',
|
||||
],
|
||||
});
|
||||
|
||||
/**
|
||||
* Turin GTT by route id, from its routes.txt (checked 2026-11-14): the trams
|
||||
* (route type 0), metro M1, and the Sassi–Superga rack tramway `79U`, which
|
||||
* the file types as a funicular (7).
|
||||
*/
|
||||
const gttRouteMode = routeModeTable({
|
||||
tram: ['3U', '4U', '7U', '9U', '10U', '13U', '15U', '16CDU', '16CSU'],
|
||||
subway: ['METROU'],
|
||||
funicular: ['79U'],
|
||||
});
|
||||
|
||||
/**
|
||||
* Bordeaux TBM tram and boat lines by route id, from TBM's static GTFS
|
||||
* routes.txt (checked 2026-11-09): the trams are numbered ids, not letters.
|
||||
@@ -571,6 +750,7 @@ function transitFeed(fields) {
|
||||
* url: string, headers?: Record<string, string>,
|
||||
* license: string, licenseUrl: string, attribution: string,
|
||||
* defaultMode: string, routeMode?: (routeId: string|null) => string,
|
||||
* routeCorrections?: Readonly<Record<string, string>>,
|
||||
* speedUnit?: 'km/h', refreshMs?: number, sourceUrl?: string,
|
||||
* creditDated?: boolean,
|
||||
* }>>}
|
||||
@@ -579,6 +759,9 @@ function transitFeed(fields) {
|
||||
* stretches the proxy's cache for an upstream that rate-limits below one
|
||||
* request per 15 s TTL. `sourceUrl` and `creditDated` add a link and a date
|
||||
* to the credit for licences that ask for them (`transitFeedCredit`).
|
||||
* `routeCorrections` names the routes whose `route_type` the operator's own
|
||||
* routes.txt gets wrong, `route_id` to the mode they really are; a correction
|
||||
* outranks the route type, which would otherwise outrank the route hint.
|
||||
*/
|
||||
export const TRANSIT_FEED_REGISTRY = Object.freeze([
|
||||
Object.freeze({
|
||||
@@ -1264,6 +1447,7 @@ export const TRANSIT_FEED_REGISTRY = Object.freeze([
|
||||
note: 'Keyless public endpoint from the metropolitan transport authority; no licence found. Same basis as Metro Transit and OVapi.',
|
||||
},
|
||||
defaultMode: 'bus',
|
||||
routeMode: tpbiRouteMode,
|
||||
}),
|
||||
transitFeed({
|
||||
id: 'roma-mobilita',
|
||||
@@ -1296,6 +1480,7 @@ export const TRANSIT_FEED_REGISTRY = Object.freeze([
|
||||
note: 'Official keyless endpoint; no licence found. Same basis as Metro Transit and OVapi. Route ids append `U` for urban lines.',
|
||||
},
|
||||
defaultMode: 'bus',
|
||||
routeMode: gttRouteMode,
|
||||
routeLabel: gttRouteLabel,
|
||||
}),
|
||||
transitFeed({
|
||||
@@ -1401,6 +1586,7 @@ export const TRANSIT_FEED_REGISTRY = Object.freeze([
|
||||
note: 'Keyless; no licence found. Same basis as Metro Transit and OVapi.',
|
||||
},
|
||||
defaultMode: 'bus',
|
||||
routeMode: ctpClujRouteMode,
|
||||
}),
|
||||
transitFeed({
|
||||
id: 'ctp-iasi',
|
||||
@@ -1567,6 +1753,8 @@ export const TRANSIT_FEED_REGISTRY = Object.freeze([
|
||||
defaultMode: 'bus',
|
||||
routeLabel: liaRouteLabel,
|
||||
routeMode: liaRouteMode,
|
||||
// LiA's routes.txt types the funicular (`8-FUNI`) as a bus (3).
|
||||
routeCorrections: Object.freeze({ '8-FUNI': 'funicular' }),
|
||||
}),
|
||||
transitFeed({
|
||||
id: 'filbleu-tours',
|
||||
@@ -1664,54 +1852,89 @@ export function transitFeedsInRange(lat, lon, slackKm = 0) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The feed's own hint for a vehicle: its fleet number when the feed maps
|
||||
* those (`vehicleMode`), else its route (`routeMode`). Null when neither knows.
|
||||
* What settles a vehicle's mode, most specific first: the feed's fleet hint
|
||||
* (`vehicleMode`, which knows a bus standing in on a tram route), else the
|
||||
* feed's correction for a route its routes.txt mistypes (`routeCorrections`),
|
||||
* else the GTFS `route_type` of its route when the record carries one, else
|
||||
* the feed's route hint (`routeMode`). Null when none of them knows.
|
||||
* @param {object} feed Registry entry.
|
||||
* @param {string|null} routeId
|
||||
* @param {string|null} vehicleId
|
||||
* @param {unknown} routeType GTFS `route_type`, when the record has one.
|
||||
* @returns {string|null}
|
||||
*/
|
||||
function hintedMode(feed, routeId, vehicleId) {
|
||||
function hintedMode(feed, routeId, vehicleId, routeType) {
|
||||
const byVehicle =
|
||||
typeof feed?.vehicleMode === 'function'
|
||||
? feed.vehicleMode(vehicleId)
|
||||
: null;
|
||||
if (byVehicle && byVehicle !== 'unknown') return byVehicle;
|
||||
const corrections = feed?.routeCorrections;
|
||||
if (routeId && corrections && Object.hasOwn(corrections, routeId))
|
||||
return corrections[routeId];
|
||||
const byType = transitModeOfRouteType(routeType);
|
||||
if (byType !== 'unknown') return byType;
|
||||
return typeof feed?.routeMode === 'function' ? feed.routeMode(routeId) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mode for a vehicle: the feed's fleet or route hint when it has one, else
|
||||
* its default.
|
||||
* Mode for a vehicle: its fleet hint, route type or route hint when one
|
||||
* knows ({@link hintedMode}), else its feed's default.
|
||||
* @param {object} feed Registry entry.
|
||||
* @param {string|null} routeId GTFS route_id from the vehicle's trip.
|
||||
* @param {string|null} [vehicleId] GTFS-RT vehicle id.
|
||||
* @param {unknown} [routeType] GTFS `route_type` of the route, when known.
|
||||
* @returns {string} One of TRANSIT_MODES.
|
||||
*/
|
||||
export function transitModeFor(feed, routeId, vehicleId = null) {
|
||||
const hinted = hintedMode(feed, routeId, vehicleId);
|
||||
export function transitModeFor(
|
||||
feed,
|
||||
routeId,
|
||||
vehicleId = null,
|
||||
routeType = null,
|
||||
) {
|
||||
const hinted = hintedMode(feed, routeId, vehicleId, routeType);
|
||||
const mode =
|
||||
hinted && hinted !== 'unknown' ? hinted : feed?.defaultMode || 'unknown';
|
||||
return TRANSIT_MODES.includes(mode) ? mode : 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the mode came from the route id rather than the feed's default. A
|
||||
* default is a guess about a fleet — TransLink's is "bus" while it publishes
|
||||
* rail in the same feed — so a consumer judging physical plausibility must
|
||||
* not hold a defaulted vehicle to a bus's limits.
|
||||
* Whether the mode came from the vehicle, its route type or its route id
|
||||
* rather than the feed's default. A default is a guess about a fleet —
|
||||
* TransLink's is "bus" while it publishes rail in the same feed — so a
|
||||
* consumer judging physical plausibility must not hold a defaulted vehicle
|
||||
* to a bus's limits.
|
||||
* @param {object} feed Registry entry.
|
||||
* @param {string|null} routeId
|
||||
* @param {string|null} [vehicleId]
|
||||
* @param {unknown} [routeType]
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function transitModeResolved(feed, routeId, vehicleId = null) {
|
||||
const hinted = hintedMode(feed, routeId, vehicleId);
|
||||
export function transitModeResolved(
|
||||
feed,
|
||||
routeId,
|
||||
vehicleId = null,
|
||||
routeType = null,
|
||||
) {
|
||||
const hinted = hintedMode(feed, routeId, vehicleId, routeType);
|
||||
return (
|
||||
Boolean(hinted) && hinted !== 'unknown' && TRANSIT_MODES.includes(hinted)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The route type a vehicle's model is chosen by: its record's, when that type
|
||||
* is a kind of the mode it was given. A fleet hint can overrule the route (a
|
||||
* bus on a tram route), and then the route's type does not describe it.
|
||||
* @param {string} mode The vehicle's mode ({@link transitModeFor}).
|
||||
* @param {unknown} routeType
|
||||
* @returns {number|null}
|
||||
*/
|
||||
export function transitModelRouteType(mode, routeType) {
|
||||
const n = transitRouteType(routeType);
|
||||
return n !== null && transitModeOfRouteType(n) === mode ? n : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The route as a rider would say it. Most feeds publish that already, so this
|
||||
* is identity for them; a feed that publishes internal keys supplies its own
|
||||
|
||||
@@ -11,9 +11,13 @@ import {
|
||||
haversineKm,
|
||||
publicTransitCatalog,
|
||||
transitFeedsInRange,
|
||||
TRANSIT_MODE_ICON,
|
||||
transitModeFor,
|
||||
transitModeOfRouteType,
|
||||
transitModeResolved,
|
||||
transitModelRouteType,
|
||||
transitRouteLabel,
|
||||
transitRouteType,
|
||||
} from './transitFeeds.js';
|
||||
import { RIO_ROUTE_NAMES } from './rioRouteNames.js';
|
||||
|
||||
@@ -729,8 +733,10 @@ test('Prague route-id ranges give metro, trains, ferries and trolleybuses their
|
||||
transitRouteLabel(feed, routeId),
|
||||
];
|
||||
assert.deepEqual(read('L22'), ['tram', '22']);
|
||||
assert.deepEqual(read('L58'), ['bus', '58'], 'trolleybus');
|
||||
assert.deepEqual(read('L49'), ['rail', 'LD'], 'Petřín funicular');
|
||||
assert.deepEqual(read('L58'), ['trolleybus', '58']);
|
||||
assert.deepEqual(read('L55'), ['bus', '55'], 'not a PID trolleybus line');
|
||||
assert.deepEqual(read('L275'), ['trolleybus', '275'], 'route type 11');
|
||||
assert.deepEqual(read('L49'), ['funicular', 'LD'], 'Petřín funicular');
|
||||
assert.deepEqual(read('L135'), ['bus', '135']);
|
||||
assert.deepEqual(read('L803'), ['bus', 'X3']);
|
||||
assert.deepEqual(read('L991'), ['subway', 'A']);
|
||||
@@ -767,7 +773,7 @@ test('Rouen Métro and Calypso are not buses; Le Havre reads its own ids', () =>
|
||||
assert.equal(transitModeFor(feed, 'TCAR:93'), 'bus');
|
||||
assert.equal(transitRouteLabel(feed, 'TAE:F'), 'F');
|
||||
const lia = getTransitFeed('lia-le-havre');
|
||||
assert.equal(transitModeFor(lia, '8-FUNI'), 'rail');
|
||||
assert.equal(transitModeFor(lia, '8-FUNI'), 'funicular');
|
||||
assert.equal(transitRouteLabel(lia, '8-C3'), 'C3');
|
||||
assert.equal(transitRouteLabel(lia, '8-09'), '9');
|
||||
assert.equal(transitRouteLabel(lia, '8-A'), 'A');
|
||||
@@ -778,3 +784,114 @@ test('data.gov.my feeds ask the proxy for no more than its 4 a minute', () => {
|
||||
assert.ok(getTransitFeed(id).refreshMs >= 30_000, id);
|
||||
}
|
||||
});
|
||||
|
||||
test('a GTFS route type names its mode, basic and extended alike', () => {
|
||||
for (const [routeType, mode] of [
|
||||
[0, 'tram'],
|
||||
[1, 'subway'],
|
||||
[2, 'rail'],
|
||||
[3, 'bus'],
|
||||
[4, 'ferry'],
|
||||
[5, 'cable_tram'],
|
||||
[6, 'aerial_lift'],
|
||||
[7, 'funicular'],
|
||||
[11, 'trolleybus'],
|
||||
[12, 'monorail'],
|
||||
[101, 'rail'],
|
||||
[106, 'rail'],
|
||||
[200, 'bus'],
|
||||
[300, 'rail'],
|
||||
[401, 'subway'],
|
||||
[405, 'monorail'],
|
||||
[702, 'bus'],
|
||||
[800, 'trolleybus'],
|
||||
[900, 'tram'],
|
||||
[1000, 'ferry'],
|
||||
[1200, 'ferry'],
|
||||
[1300, 'aerial_lift'],
|
||||
[1400, 'funicular'],
|
||||
['11', 'trolleybus'],
|
||||
])
|
||||
assert.equal(transitModeOfRouteType(routeType), mode, String(routeType));
|
||||
// Air, taxi and other services, and values GTFS does not define.
|
||||
for (const other of [8, 1100, 1500, 1700, 1800, -1, 2.5, '', 'tram', null])
|
||||
assert.equal(transitModeOfRouteType(other), 'unknown', String(other));
|
||||
assert.equal(transitRouteType(' 200 '), 200);
|
||||
assert.equal(transitRouteType('2e2'), null);
|
||||
});
|
||||
|
||||
test('every mode has an icon, and every mode a route type names is a mode', () => {
|
||||
for (const mode of TRANSIT_MODES) assert.ok(TRANSIT_MODE_ICON[mode], mode);
|
||||
for (let routeType = 0; routeType < 1800; routeType++)
|
||||
assert.ok(TRANSIT_MODES.includes(transitModeOfRouteType(routeType)));
|
||||
});
|
||||
|
||||
test('a route type settles the mode before a route hint or the default', () => {
|
||||
const mbta = getTransitFeed('mbta');
|
||||
// The hint calls every numbered MBTA route a bus; a route type is finer.
|
||||
assert.equal(transitModeFor(mbta, '71'), 'bus');
|
||||
assert.equal(transitModeFor(mbta, '71', 'y1', 11), 'trolleybus');
|
||||
assert.equal(transitModeResolved(mbta, '71', 'y1', 11), true);
|
||||
// A default-only feed is resolved by a route type alone.
|
||||
const durham = getTransitFeed('durham-region');
|
||||
assert.equal(transitModeResolved(durham, '915', 'b1'), false);
|
||||
assert.equal(transitModeFor(durham, '915', 'b1', 200), 'bus');
|
||||
assert.equal(transitModeResolved(durham, '915', 'b1', 200), true);
|
||||
// A type that names no mode leaves the hints in charge.
|
||||
assert.equal(transitModeFor(mbta, 'Red', null, 1700), 'subway');
|
||||
assert.equal(transitModeFor(mbta, 'Red', null, 'x'), 'subway');
|
||||
});
|
||||
|
||||
test('a fleet hint outranks the route type, and then the type picks no model', () => {
|
||||
const ttc = getTransitFeed('ttc-toronto');
|
||||
// A bus standing in on the 501 streetcar route (route type 0).
|
||||
const mode = transitModeFor(ttc, '501', '8400', 0);
|
||||
assert.equal(mode, 'bus');
|
||||
assert.equal(transitModelRouteType(mode, 0), null);
|
||||
assert.equal(transitModelRouteType('tram', 0), 0);
|
||||
assert.equal(transitModelRouteType('bus', 200), 200, 'a coach is a bus');
|
||||
assert.equal(transitModelRouteType('bus', null), null);
|
||||
});
|
||||
|
||||
test('a route correction outranks a mistyped route type, and only for its route', () => {
|
||||
const lia = getTransitFeed('lia-le-havre');
|
||||
// LiA's routes.txt types the funicular as a bus (3).
|
||||
assert.equal(transitModeFor(lia, '8-FUNI', null, 3), 'funicular');
|
||||
assert.equal(transitModeResolved(lia, '8-FUNI', null, 3), true);
|
||||
// A correction is a mode, so the bus type picks no model for it.
|
||||
assert.equal(transitModelRouteType('funicular', 3), null);
|
||||
// Every other route still takes its route type.
|
||||
assert.equal(transitModeFor(lia, '8-A', null, 0), 'tram');
|
||||
assert.equal(transitModeFor(lia, '8-C3', null, 3), 'bus');
|
||||
assert.equal(transitModeFor(lia, null, null, 3), 'bus');
|
||||
// Corrections are narrow: a few named routes, each to a known mode.
|
||||
for (const feed of TRANSIT_FEED_REGISTRY) {
|
||||
if (!feed.routeCorrections) continue;
|
||||
assert.ok(Object.isFrozen(feed.routeCorrections), feed.id);
|
||||
for (const [route, mode] of Object.entries(feed.routeCorrections)) {
|
||||
assert.ok(TRANSIT_MODES.includes(mode), `${feed.id} ${route}`);
|
||||
assert.notEqual(mode, 'unknown', `${feed.id} ${route}`);
|
||||
}
|
||||
}
|
||||
// An inherited key is not a correction.
|
||||
assert.equal(transitModeFor(lia, 'toString', null, 3), 'bus');
|
||||
});
|
||||
|
||||
// Route ids as each realtime feed and its operator's routes.txt carried them
|
||||
// on 2026-11-14.
|
||||
test('Bucharest, Cluj and Turin route ids give trams, trolleybuses and a funicular', () => {
|
||||
const mode = (id, routeId) => transitModeFor(getTransitFeed(id), routeId);
|
||||
assert.equal(mode('tpbi-bucuresti', '85'), 'trolleybus', 'line 74');
|
||||
assert.equal(mode('tpbi-bucuresti', '1013'), 'tram', 'line 4');
|
||||
assert.equal(mode('tpbi-bucuresti', '968'), 'subway', 'M1');
|
||||
assert.equal(mode('tpbi-bucuresti', '828'), 'bus');
|
||||
assert.equal(mode('tpbi-bucuresti', 'PV1_407'), 'bus');
|
||||
assert.equal(mode('ctp-cluj', '1'), 'trolleybus');
|
||||
assert.equal(mode('ctp-cluj', '2'), 'tram', 'line 100');
|
||||
assert.equal(mode('ctp-cluj', '6'), 'bus', 'line 19');
|
||||
assert.equal(mode('gtt-torino', '79U'), 'funicular', 'Sassi–Superga');
|
||||
assert.equal(mode('gtt-torino', '16CDU'), 'tram');
|
||||
assert.equal(mode('gtt-torino', 'METROU'), 'subway');
|
||||
assert.equal(mode('gtt-torino', '6U'), 'bus');
|
||||
assert.equal(mode('gtt-torino', null), 'bus', 'no route: the default');
|
||||
});
|
||||
@@ -21,7 +21,9 @@ import {
|
||||
* at the ~18 px the fleet renders at, by shape and not only by colour: a bus is
|
||||
* short and blunt, a tram is long and slender with articulation joints, a metro
|
||||
* car is squared off, a mainline train has a tapered nose, and a ferry is a
|
||||
* hull with a bow.
|
||||
* hull with a bow. A trolleybus trails its two poles, a cable car is a short
|
||||
* single car, a monorail the slimmest bullet, a funicular hangs off its rope,
|
||||
* and a gondola is a cabin on its cable.
|
||||
*
|
||||
* Interior detail is drawn with the same dark hairline as the edge — windscreen
|
||||
* lines, articulation joints, a deck line — because a solid dark panel would
|
||||
@@ -103,6 +105,49 @@ const HEADING_BODIES = {
|
||||
fill="white" ${STROKE}/>
|
||||
<path d="M-7.5,-6 L7.5,-6 L7.5,20 L-7.5,20 Z" ${DETAIL}/>`,
|
||||
|
||||
// ── Trolleybus: a bus a little narrower, trailing the two trolley poles
|
||||
// that reach back to its wires. Body and poles are one outline, so no
|
||||
// edge is drawn across the body.
|
||||
trolleybus: `
|
||||
<path d="M0,-41 L15,-26 L15,25 C15,28.5 13,30 9,30 L7.5,30 L7.5,42 L4,42
|
||||
L4,30 L-4,30 L-4,42 L-7.5,42 L-7.5,30 L-9,30 C-13,30 -15,28.5 -15,25
|
||||
L-15,-26 Z"
|
||||
fill="white" ${STROKE}/>
|
||||
<path d="M-10,-21 L10,-21" ${DETAIL}/>`,
|
||||
|
||||
// ── Cable car (street cable railway): a short single car, under half a
|
||||
// tram, with open platforms at both ends.
|
||||
cable_tram: `
|
||||
<path d="M0,-30 L11,-21 L11,26 C11,28 9,29 0,29 C-9,29 -11,28 -11,26
|
||||
L-11,-21 Z"
|
||||
fill="white" ${STROKE}/>
|
||||
<path d="M-11,-12 L11,-12" ${DETAIL}/>
|
||||
<path d="M-11,18 L11,18" ${DETAIL}/>`,
|
||||
|
||||
// ── Monorail: the slimmest train in the set, a rounded bullet nose over
|
||||
// its beam.
|
||||
monorail: `
|
||||
<path d="M0,-45 C3,-45 8.5,-36 8.5,-22 L8.5,42 L-8.5,42 L-8.5,-22
|
||||
C-8.5,-36 -3,-45 0,-45 Z"
|
||||
fill="white" ${STROKE}/>
|
||||
<path d="M0,-18 L0,38" ${DETAIL}/>`,
|
||||
|
||||
// ── Funicular: a squat car hauled by the rope at its upper end.
|
||||
funicular: `
|
||||
<path d="M-6,-30 L6,-30 L11,-24 L11,36 L-11,36 L-11,-24 Z
|
||||
M-1.75,-46 L1.75,-46 L1.75,-30 L-1.75,-30 Z"
|
||||
fill="white" ${STROKE}/>
|
||||
<path d="M-11,-8 L11,-8" ${DETAIL}/>
|
||||
<path d="M-11,14 L11,14" ${DETAIL}/>`,
|
||||
|
||||
// ── Gondola (aerial lift): a cabin hung on the cable that runs through
|
||||
// it; the cabin's nose is its front.
|
||||
aerial_lift: `
|
||||
<path d="M0,-18 L12,-7 L12,14 C12,16 10,17 0,17 C-10,17 -12,16 -12,14
|
||||
L-12,-7 Z
|
||||
M-2.5,-40 L2.5,-40 L2.5,46 L-2.5,46 Z"
|
||||
fill="white" ${STROKE}/>`,
|
||||
|
||||
// ── Unknown: a plain marker with a nose and nothing else, so an
|
||||
// unclassified vehicle never pretends to be a mode it is not.
|
||||
unknown: `
|
||||
@@ -146,6 +191,40 @@ const NEUTRAL_BODIES = {
|
||||
fill="white" ${STROKE}/>
|
||||
<path d="M-7.5,-13 L7.5,-13 L7.5,13 L-7.5,13 Z" ${DETAIL}/>`,
|
||||
|
||||
trolleybus: `
|
||||
<path d="M-15,-26 C-15,-30 -13,-32 -9,-32 L-7.5,-32 L-7.5,-42 L-4,-42
|
||||
L-4,-32 L4,-32 L4,-42 L7.5,-42 L7.5,-32 L9,-32 C13,-32 15,-30 15,-26
|
||||
L15,26 C15,30 13,32 9,32 L7.5,32 L7.5,42 L4,42 L4,32 L-4,32 L-4,42
|
||||
L-7.5,42 L-7.5,32 L-9,32 C-13,32 -15,30 -15,26 Z"
|
||||
fill="white" ${STROKE}/>`,
|
||||
|
||||
cable_tram: `
|
||||
<path d="M-11,-25 C-11,-27 -9,-28 0,-28 C9,-28 11,-27 11,-25
|
||||
L11,25 C11,27 9,28 0,28 C-9,28 -11,27 -11,25 Z"
|
||||
fill="white" ${STROKE}/>
|
||||
<path d="M-11,-17 L11,-17" ${DETAIL}/>
|
||||
<path d="M-11,17 L11,17" ${DETAIL}/>`,
|
||||
|
||||
monorail: `
|
||||
<path d="M0,-44 C5,-44 8.5,-38 8.5,-28 L8.5,28 C8.5,38 5,44 0,44
|
||||
C-5,44 -8.5,38 -8.5,28 L-8.5,-28 C-8.5,-38 -5,-44 0,-44 Z"
|
||||
fill="white" ${STROKE}/>
|
||||
<path d="M0,-24 L0,24" ${DETAIL}/>`,
|
||||
|
||||
funicular: `
|
||||
<path d="M-11,-30 L11,-30 L11,30 L-11,30 Z
|
||||
M-1.75,-46 L1.75,-46 L1.75,-30 L-1.75,-30 Z
|
||||
M-1.75,30 L1.75,30 L1.75,46 L-1.75,46 Z"
|
||||
fill="white" ${STROKE}/>
|
||||
<path d="M-11,-10 L11,-10" ${DETAIL}/>
|
||||
<path d="M-11,10 L11,10" ${DETAIL}/>`,
|
||||
|
||||
aerial_lift: `
|
||||
<path d="M-12,-14 C-12,-16 -10,-17 0,-17 C10,-17 12,-16 12,-14
|
||||
L12,14 C12,16 10,17 0,17 C-10,17 -12,16 -12,14 Z
|
||||
M-2.5,-46 L2.5,-46 L2.5,46 L-2.5,46 Z"
|
||||
fill="white" ${STROKE}/>`,
|
||||
|
||||
unknown: `
|
||||
<path d="M-10,-26 C-10,-30 -7,-32 0,-32 C7,-32 10,-30 10,-26
|
||||
L10,26 C10,30 7,32 0,32 C-7,32 -10,30 -10,26 Z"
|
||||
|
||||
@@ -62,7 +62,7 @@ test('the raster cache is bounded by kind, size and halo — never by call count
|
||||
assert.ok(transitIconCacheSize() <= before + 3 + TRANSIT_ICON_KINDS.length);
|
||||
});
|
||||
|
||||
test('all 72 variants retain the specified final halo at both display sizes', async () => {
|
||||
test('every variant retains the specified final halo at both display sizes', async () => {
|
||||
const { presetSpriteScale, presetSpriteOutlinePx } =
|
||||
await import('./transitPresetStyle.js');
|
||||
for (const kind of TRANSIT_ICON_KINDS) {
|
||||
@@ -97,7 +97,8 @@ test('all 72 variants retain the specified final halo at both display sizes', as
|
||||
}
|
||||
for (let i = 0; i < 100; i++)
|
||||
transitIcon('invalid', i, { haloScreenPx: i, screenPx: i });
|
||||
assert.equal(transitIconCacheSize(), 72);
|
||||
// Every kind at two sizes, three profiles, with and without a course.
|
||||
assert.equal(transitIconCacheSize(), TRANSIT_ICON_KINDS.length * 12);
|
||||
});
|
||||
|
||||
test('every mono raster preserves the normal silhouette mask and aspect at CRT display size', async () => {
|
||||
@@ -197,6 +198,11 @@ test('sensor raster halo stays dark across its screen-space band on a white roof
|
||||
subway: 11.5,
|
||||
rail: 10.5,
|
||||
ferry: 12.5,
|
||||
trolleybus: 15,
|
||||
cable_tram: 11,
|
||||
monorail: 8.5,
|
||||
funicular: 11,
|
||||
aerial_lift: 12,
|
||||
unknown: 10,
|
||||
};
|
||||
for (const selected of [false, true]) {
|
||||
|
||||
@@ -60,9 +60,14 @@ const SPRITE_STYLE = Object.freeze({
|
||||
outlinePx: 1.25,
|
||||
rgbaByMode: Object.freeze({
|
||||
bus: Object.freeze([0, 255, 102, 1]),
|
||||
trolleybus: Object.freeze([170, 255, 0, 1]),
|
||||
tram: Object.freeze([255, 179, 0, 1]),
|
||||
cable_tram: Object.freeze([255, 120, 0, 1]),
|
||||
subway: Object.freeze([255, 59, 48, 1]),
|
||||
monorail: Object.freeze([255, 64, 170, 1]),
|
||||
rail: Object.freeze([215, 139, 255, 1]),
|
||||
funicular: Object.freeze([255, 214, 140, 1]),
|
||||
aerial_lift: Object.freeze([90, 140, 255, 1]),
|
||||
ferry: Object.freeze([58, 208, 255, 1]),
|
||||
unknown: Object.freeze([255, 255, 255, 1]),
|
||||
}),
|
||||
@@ -72,9 +77,14 @@ const SPRITE_STYLE = Object.freeze({
|
||||
/** Modes with a bracket tier of their own. */
|
||||
const TIER_MODES = Object.freeze([
|
||||
'bus',
|
||||
'trolleybus',
|
||||
'tram',
|
||||
'cable_tram',
|
||||
'subway',
|
||||
'monorail',
|
||||
'rail',
|
||||
'funicular',
|
||||
'aerial_lift',
|
||||
'ferry',
|
||||
'unknown',
|
||||
]);
|
||||
@@ -128,7 +138,7 @@ export function presetSpriteOutlinePx(styleName, selected = false) {
|
||||
|
||||
/**
|
||||
* Detection-overlay tier key for a transit mode. Every theme's `tiers`
|
||||
* table carries these six, so the bracket colour is per mode in every
|
||||
* table carries one per mode, so the bracket colour is per mode in every
|
||||
* preset. Unlike traffic, transit sets its tier keyless as well: the feeds
|
||||
* are keyless, and there is no separate keyless experience to keep
|
||||
* byte-identical.
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
decodeVehiclePositions,
|
||||
} from './gtfsRealtime.js';
|
||||
import { getTransitFeed } from './transitFeeds.js';
|
||||
export { transitModeFor, transitRouteType } from './transitFeeds.js';
|
||||
|
||||
/** Fresh window: a snapshot younger than this is served without refetching. */
|
||||
export const TRANSIT_PROXY_TTL_MS = 15_000;
|
||||
|
||||
@@ -3962,5 +3962,10 @@
|
||||
"Reset view reloads the app without the view restored from the address bar.": "تعيد «إعادة ضبط العرض» تحميل التطبيق دون العرض المستعاد من شريط العنوان.",
|
||||
"Reload": "إعادة التحميل",
|
||||
"Retry": "إعادة المحاولة",
|
||||
"Reset view": "إعادة ضبط العرض"
|
||||
"Reset view": "إعادة ضبط العرض",
|
||||
"Trolleybus": "ترولي باص",
|
||||
"Cable car": "ترام كابلي",
|
||||
"Monorail": "قطار أحادي السكة",
|
||||
"Funicular": "قطار جبلي مائل",
|
||||
"Gondola": "تلفريك"
|
||||
}
|
||||
@@ -3614,5 +3614,10 @@
|
||||
"Reset view reloads the app without the view restored from the address bar.": "Ansicht zurücksetzen lädt die App ohne die aus der Adressleiste wiederhergestellte Ansicht neu.",
|
||||
"Reload": "Neu laden",
|
||||
"Retry": "Erneut versuchen",
|
||||
"Reset view": "Ansicht zurücksetzen"
|
||||
"Reset view": "Ansicht zurücksetzen",
|
||||
"Trolleybus": "Oberleitungsbus",
|
||||
"Cable car": "Kabelstraßenbahn",
|
||||
"Monorail": "Einschienenbahn",
|
||||
"Funicular": "Standseilbahn",
|
||||
"Gondola": "Gondelbahn"
|
||||
}
|
||||
@@ -3701,5 +3701,10 @@
|
||||
"Reset view reloads the app without the view restored from the address bar.": "Restablecer la vista recarga la app sin la vista restaurada desde la barra de direcciones.",
|
||||
"Reload": "Recargar",
|
||||
"Retry": "Reintentar",
|
||||
"Reset view": "Restablecer la vista"
|
||||
"Reset view": "Restablecer la vista",
|
||||
"Trolleybus": "Trolebús",
|
||||
"Cable car": "Tranvía de cable",
|
||||
"Monorail": "Monorraíl",
|
||||
"Funicular": "Funicular",
|
||||
"Gondola": "Góndola"
|
||||
}
|
||||
@@ -3701,5 +3701,10 @@
|
||||
"Reset view reloads the app without the view restored from the address bar.": "Réinitialiser la vue recharge l’app sans la vue restaurée depuis la barre d’adresse.",
|
||||
"Reload": "Recharger",
|
||||
"Retry": "Réessayer",
|
||||
"Reset view": "Réinitialiser la vue"
|
||||
"Reset view": "Réinitialiser la vue",
|
||||
"Trolleybus": "Trolleybus",
|
||||
"Cable car": "Tramway à câble",
|
||||
"Monorail": "Monorail",
|
||||
"Funicular": "Funiculaire",
|
||||
"Gondola": "Télécabine"
|
||||
}
|
||||
@@ -3701,5 +3701,10 @@
|
||||
"Reset view reloads the app without the view restored from the address bar.": "\"איפוס תצוגה\" טוען מחדש את האפליקציה בלי התצוגה ששוחזרה משורת הכתובת.",
|
||||
"Reload": "טען מחדש",
|
||||
"Retry": "נסה שוב",
|
||||
"Reset view": "איפוס תצוגה"
|
||||
"Reset view": "איפוס תצוגה",
|
||||
"Trolleybus": "טרוליבוס",
|
||||
"Cable car": "חשמלית כבלים",
|
||||
"Monorail": "מונורייל",
|
||||
"Funicular": "פוניקולר",
|
||||
"Gondola": "רכבל"
|
||||
}
|
||||
@@ -3701,5 +3701,10 @@
|
||||
"Reset view reloads the app without the view restored from the address bar.": "Ripristina vista ricarica l’app senza la vista ripristinata dalla barra degli indirizzi.",
|
||||
"Reload": "Ricarica",
|
||||
"Retry": "Riprova",
|
||||
"Reset view": "Ripristina vista"
|
||||
"Reset view": "Ripristina vista",
|
||||
"Trolleybus": "Filobus",
|
||||
"Cable car": "Tram a fune",
|
||||
"Monorail": "Monorotaia",
|
||||
"Funicular": "Funicolare",
|
||||
"Gondola": "Cabinovia"
|
||||
}
|
||||
@@ -3527,5 +3527,10 @@
|
||||
"Reset view reloads the app without the view restored from the address bar.": "「ビューをリセット」は、アドレスバーから復元したビューを使わずにアプリを再読み込みします。",
|
||||
"Reload": "再読み込み",
|
||||
"Retry": "再試行",
|
||||
"Reset view": "ビューをリセット"
|
||||
"Reset view": "ビューをリセット",
|
||||
"Trolleybus": "トロリーバス",
|
||||
"Cable car": "ケーブルトラム",
|
||||
"Monorail": "モノレール",
|
||||
"Funicular": "ケーブルカー",
|
||||
"Gondola": "ゴンドラ"
|
||||
}
|
||||
@@ -3701,5 +3701,10 @@
|
||||
"Reset view reloads the app without the view restored from the address bar.": "Redefinir vista recarrega o app sem a vista restaurada da barra de endereços.",
|
||||
"Reload": "Recarregar",
|
||||
"Retry": "Tentar novamente",
|
||||
"Reset view": "Redefinir vista"
|
||||
"Reset view": "Redefinir vista",
|
||||
"Trolleybus": "Trólebus",
|
||||
"Cable car": "Bonde a cabo",
|
||||
"Monorail": "Monotrilho",
|
||||
"Funicular": "Funicular",
|
||||
"Gondola": "Teleférico"
|
||||
}
|
||||
@@ -3788,5 +3788,10 @@
|
||||
"Reset view reloads the app without the view restored from the address bar.": "«Сбросить вид» перезагружает приложение без вида, восстановленного из адресной строки.",
|
||||
"Reload": "Перезагрузить",
|
||||
"Retry": "Повторить",
|
||||
"Reset view": "Сбросить вид"
|
||||
"Reset view": "Сбросить вид",
|
||||
"Trolleybus": "Троллейбус",
|
||||
"Cable car": "Канатный трамвай",
|
||||
"Monorail": "Монорельс",
|
||||
"Funicular": "Фуникулёр",
|
||||
"Gondola": "Канатная дорога"
|
||||
}
|
||||
@@ -3614,5 +3614,10 @@
|
||||
"Reset view reloads the app without the view restored from the address bar.": "”Återställ vy” laddar om appen utan vyn som återställdes från adressfältet.",
|
||||
"Reload": "Ladda om",
|
||||
"Retry": "Försök igen",
|
||||
"Reset view": "Återställ vy"
|
||||
"Reset view": "Återställ vy",
|
||||
"Trolleybus": "Trådbuss",
|
||||
"Cable car": "Kabelspårvagn",
|
||||
"Monorail": "Enskensbana",
|
||||
"Funicular": "Bergbana",
|
||||
"Gondola": "Kabinbana"
|
||||
}
|
||||
@@ -3788,5 +3788,10 @@
|
||||
"Reset view reloads the app without the view restored from the address bar.": "«Скинути вигляд» перезавантажує застосунок без вигляду, відновленого з адресного рядка.",
|
||||
"Reload": "Перезавантажити",
|
||||
"Retry": "Повторити",
|
||||
"Reset view": "Скинути вигляд"
|
||||
"Reset view": "Скинути вигляд",
|
||||
"Trolleybus": "Тролейбус",
|
||||
"Cable car": "Канатний трамвай",
|
||||
"Monorail": "Монорейка",
|
||||
"Funicular": "Фунікулер",
|
||||
"Gondola": "Канатна дорога"
|
||||
}
|
||||
@@ -3527,5 +3527,10 @@
|
||||
"Reset view reloads the app without the view restored from the address bar.": "“重置视图”会重新加载应用,不使用从地址栏恢复的视图。",
|
||||
"Reload": "重新加载",
|
||||
"Retry": "重试",
|
||||
"Reset view": "重置视图"
|
||||
"Reset view": "重置视图",
|
||||
"Trolleybus": "无轨电车",
|
||||
"Cable car": "缆索电车",
|
||||
"Monorail": "单轨列车",
|
||||
"Funicular": "缆索铁路",
|
||||
"Gondola": "缆车"
|
||||
}
|
||||
@@ -3527,5 +3527,10 @@
|
||||
"Reset view reloads the app without the view restored from the address bar.": "「重置視圖」會重新加載應用,不使用從地址欄恢復的視圖。",
|
||||
"Reload": "重新加載",
|
||||
"Retry": "重試",
|
||||
"Reset view": "重置視圖"
|
||||
"Reset view": "重置視圖",
|
||||
"Trolleybus": "無軌電車",
|
||||
"Cable car": "纜索電車",
|
||||
"Monorail": "單軌列車",
|
||||
"Funicular": "纜索鐵路",
|
||||
"Gondola": "纜車"
|
||||
}
|
||||
@@ -291,6 +291,7 @@
|
||||
"FOV {value}°",
|
||||
"FT",
|
||||
"Folk",
|
||||
"Funicular",
|
||||
"Funk",
|
||||
"GEO",
|
||||
"GOOGLE STREET VIEW",
|
||||
@@ -501,6 +502,7 @@
|
||||
"MINI",
|
||||
"MISSION",
|
||||
"MM",
|
||||
"Monorail",
|
||||
"MSG/S",
|
||||
"Metal",
|
||||
"Minimal",
|
||||
@@ -571,6 +573,7 @@
|
||||
"TAGS · {tags}",
|
||||
"TEMP",
|
||||
"TLE",
|
||||
"Trolleybus",
|
||||
"TYPE",
|
||||
"Tallinn",
|
||||
"Techno",
|
||||
@@ -950,6 +953,7 @@
|
||||
"FOV {value}°",
|
||||
"FT",
|
||||
"Folk",
|
||||
"Funicular",
|
||||
"Funk",
|
||||
"GEO",
|
||||
"GOOGLE STREET VIEW",
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import {
|
||||
transitModeFor,
|
||||
transitModeResolved,
|
||||
transitModelRouteType,
|
||||
} from '../../data/transitFeeds.js';
|
||||
import {
|
||||
attachFloorToPlace,
|
||||
@@ -222,12 +223,19 @@ export function createIngestion({ state, services, parts, source }) {
|
||||
for (const record of records) {
|
||||
if (isStaleVehicleFix(record, now, fetchedAt)) continue;
|
||||
const key = transitVehicleKey(feed.id, record.id);
|
||||
const mode = transitModeFor(feed, record.routeId, record.id);
|
||||
const mode = transitModeFor(
|
||||
feed,
|
||||
record.routeId,
|
||||
record.id,
|
||||
record.routeType,
|
||||
);
|
||||
const modeInferred = !transitModeResolved(
|
||||
feed,
|
||||
record.routeId,
|
||||
record.id,
|
||||
record.routeType,
|
||||
);
|
||||
const routeType = transitModelRouteType(mode, record.routeType);
|
||||
// A stamp ahead of the fetch is a clock fault; the fetch time bounds it.
|
||||
const stamped = vehicleReportTimeMs(record, fetchedAt);
|
||||
const reportAt =
|
||||
@@ -267,6 +275,7 @@ export function createIngestion({ state, services, parts, source }) {
|
||||
continue;
|
||||
}
|
||||
if (entry.mode !== mode) parts.rendering.paintMode(entry, mode);
|
||||
entry.routeType = routeType;
|
||||
entry.modeInferred = modeInferred;
|
||||
updatePlayback(entry, now, monoNow);
|
||||
parts.rendering.schedulePlayback(entry);
|
||||
@@ -320,6 +329,8 @@ export function createIngestion({ state, services, parts, source }) {
|
||||
feedId: feed.id,
|
||||
marker: null,
|
||||
mode,
|
||||
// The GTFS route type its model is chosen by, when it has one.
|
||||
routeType,
|
||||
modeInferred,
|
||||
heightM: prior === null ? 0 : prior,
|
||||
heightCell: height.cell,
|
||||
|
||||
@@ -11,9 +11,14 @@ import {
|
||||
} from '../../data/contactPlayback.js';
|
||||
const MODE_MAX_SPEED_MPS = Object.freeze({
|
||||
bus: 36, // ~130 km/h — a coach on a motorway
|
||||
trolleybus: 28, // ~100 km/h — wired to the street
|
||||
tram: 28, // ~100 km/h — interurban light rail
|
||||
cable_tram: 12, // ~43 km/h — the cable runs at a walking-pace multiple
|
||||
subway: 42, // ~150 km/h
|
||||
monorail: 42, // ~150 km/h
|
||||
rail: 111, // ~400 km/h — high-speed services exist on some feeds
|
||||
funicular: 16, // ~58 km/h — the fastest lines
|
||||
aerial_lift: 15, // ~54 km/h — the fastest gondolas and trams
|
||||
ferry: 25, // ~90 km/h — fast catamarans
|
||||
unknown: 56, // ~200 km/h
|
||||
});
|
||||
|
||||
@@ -189,9 +189,14 @@ export const SELECTED_CARD_REFRESH_MS = 250;
|
||||
/** Mode palette: distinct at a glance, none reused by flights (white/cyan), military (amber), or vessels. */
|
||||
export const TRANSIT_MODE_COLORS = Object.freeze({
|
||||
bus: '#5EF08A',
|
||||
trolleybus: '#B6F04A',
|
||||
tram: '#FFC24A',
|
||||
cable_tram: '#FF8F4A',
|
||||
subway: '#FF4538',
|
||||
monorail: '#FF6FB8',
|
||||
rail: '#D9A6FF',
|
||||
funicular: '#E8C98A',
|
||||
aerial_lift: '#7FA8FF',
|
||||
ferry: '#5FD6FF',
|
||||
unknown: '#D8DDE5',
|
||||
});
|
||||
@@ -526,9 +531,14 @@ const UNDERGROUND_MODES = new Set(['subway']);
|
||||
/** The mode in plain words, for a card a person reads rather than scans. */
|
||||
const TRANSIT_MODE_WORD = Object.freeze({
|
||||
bus: msg('Bus'),
|
||||
trolleybus: msg('Trolleybus'),
|
||||
tram: msg('Tram'),
|
||||
cable_tram: msg('Cable car'),
|
||||
subway: msg('Subway'),
|
||||
monorail: msg('Monorail'),
|
||||
rail: msg('Train'),
|
||||
funicular: msg('Funicular'),
|
||||
aerial_lift: msg('Gondola'),
|
||||
ferry: msg('Ferry'),
|
||||
unknown: msg('Transit vehicle'),
|
||||
});
|
||||
@@ -536,9 +546,14 @@ const TRANSIT_MODE_WORD = Object.freeze({
|
||||
/** Short, fixed-width mode word for the detection card's class field. */
|
||||
const TRANSIT_MODE_ABBR = Object.freeze({
|
||||
bus: 'BUS',
|
||||
trolleybus: 'TROLLEY',
|
||||
tram: 'TRAM',
|
||||
cable_tram: 'CABLE',
|
||||
subway: 'METRO',
|
||||
monorail: 'MONO',
|
||||
rail: 'RAIL',
|
||||
funicular: 'FUNI',
|
||||
aerial_lift: 'GONDOLA',
|
||||
ferry: 'FERRY',
|
||||
unknown: 'TRANSIT',
|
||||
});
|
||||
|
||||
@@ -531,9 +531,15 @@ export function createRendering({ state, services, parts }) {
|
||||
groundLiftM,
|
||||
};
|
||||
}
|
||||
if (item.mode !== entry.mode) {
|
||||
if (item.mode !== entry.mode || item.routeType !== entry.routeType) {
|
||||
item.mode = entry.mode;
|
||||
item.spec = transitModelSpec(set, { mode: entry.mode });
|
||||
item.routeType = entry.routeType;
|
||||
// The route type picks the finer model (a coach, a high-speed set);
|
||||
// the mode is the fallback for a type the model table lacks.
|
||||
item.spec = transitModelSpec(set, {
|
||||
routeType: entry.routeType,
|
||||
mode: entry.mode,
|
||||
});
|
||||
item.tint =
|
||||
MODE_CESIUM_COLORS[entry.mode] || MODE_CESIUM_COLORS.unknown;
|
||||
}
|
||||
|
||||
@@ -8,16 +8,9 @@ import {
|
||||
} from '../../data/contactPlayback.js';
|
||||
import { createContactTrailRenderer } from '../../data/contactTrailRenderer.js';
|
||||
import { fixDistanceM } from './movement.js';
|
||||
import { TRANSIT_MODE_COLORS } from './policy.js';
|
||||
import { transitStyleProfile } from '../../data/transitPresetStyle.js';
|
||||
|
||||
const MODE_COLOR = {
|
||||
bus: '#5EF08A',
|
||||
tram: '#FFC24A',
|
||||
subway: '#FF4538',
|
||||
rail: '#D9A6FF',
|
||||
ferry: '#5FD6FF',
|
||||
unknown: '#D8DDE5',
|
||||
};
|
||||
// Final Cesium batching uses 3,152 bytes per two-lane edge.
|
||||
// The 1,278-instance body plus 64 bytes/instance reserve stays below 2 MiB.
|
||||
export const TRAIL_VERTEX_LIMIT = 640;
|
||||
@@ -41,7 +34,7 @@ export function createTrails({ state, services, parts, source }) {
|
||||
renderer?.setStyle(
|
||||
transitStyleProfile(state._stylePreset) === 'mono'
|
||||
? '#FFFFFF'
|
||||
: MODE_COLOR[selected?.mode] || MODE_COLOR.unknown,
|
||||
: TRANSIT_MODE_COLORS[selected?.mode] || TRANSIT_MODE_COLORS.unknown,
|
||||
);
|
||||
}
|
||||
function releaseSelectionGeometry(entry) {
|
||||
|
||||
@@ -139,9 +139,14 @@ export const DETECTION_THEME_MAP = Object.freeze({
|
||||
veh_free: '#00ff66',
|
||||
veh_nodata: '#c9c9c9',
|
||||
transit_bus: '#5EF08A',
|
||||
transit_trolleybus: '#B6F04A',
|
||||
transit_tram: '#FFC24A',
|
||||
transit_cable_tram: '#FF8F4A',
|
||||
transit_subway: '#FF4538',
|
||||
transit_monorail: '#FF6FB8',
|
||||
transit_rail: '#D9A6FF',
|
||||
transit_funicular: '#E8C98A',
|
||||
transit_aerial_lift: '#7FA8FF',
|
||||
transit_ferry: '#5FD6FF',
|
||||
transit_unknown: '#D8DDE5',
|
||||
},
|
||||
@@ -169,9 +174,14 @@ export const DETECTION_THEME_MAP = Object.freeze({
|
||||
veh_free: '#45d8ff',
|
||||
veh_nodata: '#dcdcdc',
|
||||
transit_bus: '#5EF08A',
|
||||
transit_trolleybus: '#B6F04A',
|
||||
transit_tram: '#FFC24A',
|
||||
transit_cable_tram: '#FF8F4A',
|
||||
transit_subway: '#FF4538',
|
||||
transit_monorail: '#FF6FB8',
|
||||
transit_rail: '#D9A6FF',
|
||||
transit_funicular: '#E8C98A',
|
||||
transit_aerial_lift: '#7FA8FF',
|
||||
transit_ferry: '#5FD6FF',
|
||||
transit_unknown: '#D8DDE5',
|
||||
},
|
||||
@@ -199,9 +209,14 @@ export const DETECTION_THEME_MAP = Object.freeze({
|
||||
veh_free: '#2ecc71',
|
||||
veh_nodata: '#d8d8d8',
|
||||
transit_bus: '#5EF08A',
|
||||
transit_trolleybus: '#B6F04A',
|
||||
transit_tram: '#FFC24A',
|
||||
transit_cable_tram: '#FF8F4A',
|
||||
transit_subway: '#FF4538',
|
||||
transit_monorail: '#FF6FB8',
|
||||
transit_rail: '#D9A6FF',
|
||||
transit_funicular: '#E8C98A',
|
||||
transit_aerial_lift: '#7FA8FF',
|
||||
transit_ferry: '#5FD6FF',
|
||||
transit_unknown: '#D8DDE5',
|
||||
},
|
||||
@@ -229,9 +244,14 @@ export const DETECTION_THEME_MAP = Object.freeze({
|
||||
veh_free: '#2ecc71',
|
||||
veh_nodata: '#c9c9c9',
|
||||
transit_bus: '#5EF08A',
|
||||
transit_trolleybus: '#B6F04A',
|
||||
transit_tram: '#FFC24A',
|
||||
transit_cable_tram: '#FF8F4A',
|
||||
transit_subway: '#FF4538',
|
||||
transit_monorail: '#FF6FB8',
|
||||
transit_rail: '#D9A6FF',
|
||||
transit_funicular: '#E8C98A',
|
||||
transit_aerial_lift: '#7FA8FF',
|
||||
transit_ferry: '#5FD6FF',
|
||||
transit_unknown: '#D8DDE5',
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { transitModeFor } from '../data/transitFeeds.js';
|
||||
import { validTransitIdentifier } from './transitHistory.js';
|
||||
export { validTransitIdentifier } from './transitHistory.js';
|
||||
/** Bounded, process-local operator position history. No upstream or scene access. */
|
||||
@@ -328,12 +329,12 @@ export function createTransitHistory({
|
||||
t > now + 10000
|
||||
)
|
||||
continue;
|
||||
const byVehicle = feed.vehicleMode?.(record.id);
|
||||
const mode =
|
||||
(byVehicle !== 'unknown' && byVehicle) ||
|
||||
feed.routeMode?.(record.routeId) ||
|
||||
feed.defaultMode ||
|
||||
'unknown';
|
||||
const mode = transitModeFor(
|
||||
feed,
|
||||
record.routeId,
|
||||
record.id,
|
||||
record.routeType,
|
||||
);
|
||||
const context = contextOf(record, mode);
|
||||
if (!context) continue;
|
||||
const key = `${feed.id}\u0000${record.id}`;
|
||||
|
||||
@@ -302,7 +302,23 @@ const layers = [
|
||||
},
|
||||
{
|
||||
id: 'transit',
|
||||
aliases: ['buses', 'trains', 'public transit', 'trams', 'subway'],
|
||||
aliases: [
|
||||
'buses',
|
||||
'trains',
|
||||
'public transit',
|
||||
'trams',
|
||||
'subway',
|
||||
'trolleybus',
|
||||
'trolleybuses',
|
||||
'gondola',
|
||||
'gondolas',
|
||||
'cable car',
|
||||
'cable cars',
|
||||
'funicular',
|
||||
'funiculars',
|
||||
'monorail',
|
||||
'monorails',
|
||||
],
|
||||
context: false,
|
||||
query: {
|
||||
fields: {
|
||||
|
||||
Reference in new issue
Block a user