map
Map customization: MapLibre sources and layers, per-city tile and routing overrides, per-layer overrides, and default layer visibility.
api.map. 10 members.
| Member | Returns | Summary |
|---|---|---|
registerSource(id, source) | void | Register a MapLibre source. Added to the map and re-added after every style reload. |
registerLayer(layer, beforeId?) | void | Register a MapLibre layer. Added to the map and re-added after every style reload. |
registerStyle(styleUrl) | void | Record a MapLibre style URL in the custom style list. |
setTileURLOverride(override) | void | Replace the tile URL templates the map style uses for a city. |
setLayerOverride(override) | void | Override the source-layer, filter and paint of one basemap layer. |
setDefaultLayerVisibility(cityCode, layerDefaults) | void | Set layer visibility defaults for a city. Merged into the player's layer settings when the city loads. |
getDefaultLayerVisibility(cityCode) | Partial<Record<string, boolean>> or undefined | The layer visibility defaults stored for a city, or undefined. |
setRoutingServiceOverride(override) | void | Register a driving-directions service for a city. Inside the game only map.queryRoute uses the registry; map.getRoutingServiceOverride returns the stored entry. |
getRoutingServiceOverride(cityCode) | RoutingServiceOverride or undefined | The routing service registered for a city, or undefined. |
queryRoute(cityCode, origin, destination) | Promise of object or null | Fetch a driving route from the city's registered routing service and normalise the response. |
Custom Sources, Layers and Styles#
registerSource#
api.map.registerSource(id: string, source: MapSource): voidRegister a MapLibre source. Added to the map and re-added after every style reload.
A second registration with the same id replaces the stored source. When the map style is already loaded and the map has no source with that id, the source is added immediately. On map ready, and after each MapLibre styledata event, every registered source missing from the map is added again. The registry is cleared on reloadMods. Sources already on the map stay there. Logs [Modding API] Registering custom source: <id> (development builds only).
| Param | Type | Default | Description |
|---|---|---|---|
id | string | required | source id. |
source | MapSource | required | MapLibre source specification. |
Errors:
- missing
idorsource: logs[Modding API] Invalid source - must have id and source object, registers nothing. - exception: logs
[Modding API] Error registering source <id>:. - re-adding fails on map ready or after a style reload: records an execution error
Error adding custom source <id>: <message>, logged at warn level as[Mod Error] execution: ...; the other sources are still re-added.
Side effects: source registry updated; MapLibre addSource on a loaded map.
// re-added automatically after every style reload
api.map.registerSource('my-heat', {
type: 'vector',
tiles: ['http://127.0.0.1:8080/heat/{z}/{x}/{y}.mvt'],
maxzoom: 14,
});registerLayer#
api.map.registerLayer(layer: MapLayer, beforeId?: string): voidRegister a MapLibre layer. Added to the map and re-added after every style reload.
A layer with an id already registered replaces the earlier entry. When the map style is already loaded, an existing layer with the same id is removed and the layer is added immediately with beforeId. On map ready, and after each MapLibre styledata event, every registered layer whose id is missing from the map is added again with its beforeId. The registry is cleared on reloadMods. Layers already on the map stay there. Logs [Modding API] Registering custom layer: <id> (development builds only) (with (before <beforeId>) when given).
| Param | Type | Default | Description |
|---|---|---|---|
layer | MapLayer | required | MapLibre layer specification; id required. |
beforeId | string | none | passed to MapLibre addLayer as its beforeId, on the immediate add and on every re-add. |
Errors:
- missing
layerorlayer.id: logs[Modding API] Invalid layer - must have id, registers nothing. - exception: logs
[Modding API] Error registering layer <id>:. - re-adding fails on map ready or after a style reload: records an execution error
Error adding custom layer <id>: <message>, logged at warn level as[Mod Error] execution: ...; the other layers are still re-added.
Side effects: __beforeId written onto the layer object when beforeId is given; layer registry updated; MapLibre removeLayer/addLayer on a loaded map.
api.map.registerSource('my-zones', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });
api.map.registerLayer(
{ id: 'my-zones-fill', type: 'fill', source: 'my-zones', paint: { 'fill-color': '#22c55e', 'fill-opacity': 0.2 } },
'road-labels' // beforeId: draw below the road labels
);Layer Z-Order with beforeId#
beforeId names an existing MapLibre layer; the new layer is drawn directly below it. Without it, MapLibre adds the layer on top of its stack. It orders only against MapLibre layers; the deck.gl overlays cannot be targeted. The game's road label layer is road-labels.
api.map.registerSource('my-zones', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });
api.map.registerLayer(
{ id: 'my-zones-fill', type: 'fill', source: 'my-zones', paint: { 'fill-color': '#088', 'fill-opacity': 0.3 } },
'road-labels' // drawn below the road labels
);Layer Persistence#
On map ready and after every MapLibre styledata event, each registered source missing from the map is added again, and each registered layer whose id is missing is added again with its stored beforeId. A style reload needs no handling in the mod. reloadMods clears both registries; sources and layers already on the map stay there.
registerStyle#
api.map.registerStyle(styleUrl: string): voidRecord a MapLibre style URL in the custom style list.
No game code reads the list, so a registered style is never applied to the map. The list is cleared on reloadMods. Logs [Modding API] Registering custom style: <url> (development builds only).
Parameter styleUrl (string): style URL; must be a non-empty string.
Errors:
- missing or non-string
styleUrl: logs[Modding API] Invalid style URL, records nothing. - exception: logs
[Modding API] Error registering style:.
Side effects: custom style list appended.
// recorded only: no game code applies registered styles; use registerSource + registerLayer
api.map.registerStyle('http://127.0.0.1:8080/style.json');Override Tile URLs#
setTileURLOverride#
api.map.setTileURLOverride(override: TileURLOverride): voidReplace the tile URL templates the map style uses for a city.
The style builder reads the override when it builds the style. tilesUrl replaces map://<code>/tiles/{z}/{x}/{y}.mvt and foundationTilesUrl replaces map://<code>/foundations/{z}/{x}/{y}.mvt. The source maxzoom is maxZoom, or 15 when omitted. The lookup is keyed by the plain city code. A registered city's uid is collapsed to its code before the lookup. Logs [Modding API] Setting tile URL override for <code> (development builds only). In the desktop app the request filter cancels a request whose URL does not match the app's fixed network allowlist of exact hosts; the allowlist admits plain http:// on 127.0.0.1 and localhost; serve tiles from the player's machine.
Parameter override (TileURLOverride): city code and the URL templates to use.
Errors:
- missing
overrideoroverride.cityCode: logs[Modding API] Invalid tile URL override - must have cityCode, stores nothing. - exception: logs
[Modding API] Error setting tile URL override:.
Side effects: tile override registry entry replaced; applied on the next style build.
api.map.setTileURLOverride({
cityCode: 'HFX',
tilesUrl: 'http://127.0.0.1:8080/HFX/{z}/{x}/{y}.mvt',
foundationTilesUrl: 'http://127.0.0.1:8080/HFX/foundations/{z}/{x}/{y}.mvt',
maxZoom: 15,
});Override Layer Properties#
setLayerOverride#
api.map.setLayerOverride(override: LayerSourceOverride): voidOverride the source-layer, filter and paint of one basemap layer.
The style builder honours overrides for water, parks-large, parks-small, commercial and airports (source-layer, filter and paint) and for buildings-3d (filter only, combined with the demolished-buildings filter). paint entries are merged over the layer's defaults. filter replaces the layer's filter. When the map already has the layer, filter and paint are applied to the live layer at once. In that case a sourceLayer also logs a warning [Modding API] setLayerOverride: source-layer for <id> applies on the next style load, for layers the style builder honours (filter/paint apply now). At the next style build the style builder applies a stored sourceLayer for water, parks-large, parks-small, commercial and airports; for buildings-3d it applies only filter, and other layer ids are not read. parks-large and parks-small read the same parks source-layer and split it with the default filters ['>=', ['get', 'area'], 100000] and ['<', ['get', 'area'], 100000]. A filter override replaces that split. commercial draws commercial and college areas from one source-layer, coloured by the type feature property ('college' or anything else). No separate college layer exists. The default fill-extrusion-opacity of parks-large, parks-small and commercial comes from GAME_COLORS.AREA_VISUALS (a flat opacity or a zoom fade). A paint override of that property replaces it. An override for any other layer id is stored and never read by the style builder. Logs [Modding API] Setting layer override for <id> (development builds only).
Parameter override (LayerSourceOverride): layer id and the properties to override.
Errors:
- missing
overrideoroverride.layerId: logs[Modding API] Invalid layer override - must have layerId, stores nothing. - exception: logs
[Modding API] Error setting layer override <id>:.
Side effects: layer override registry entry replaced; MapLibre setFilter and setPaintProperty on a live layer.
Changed: sourceLayer is no longer passed to MapLibre's setLayoutProperty on a live layer; the style builder applies it on the next style load. filter and paint applied to the live layer before too (PR #1090).
api.map.setLayerOverride({ layerId: 'parks-large', sourceLayer: 'landuse', filter: ['==', ['get', 'kind'], 'park'] });Commercial & College Districts#
The commercial layer draws commercial districts and college campuses from one source-layer. A feature with type: 'college' gets the map theme's college color; every other feature gets the commercial color. Both share one fade and opacity (GAME_COLORS.AREA_VISUALS). No separate college layer exists, so a custom tileset that stores campuses elsewhere has to be remapped into this layer, with campus features carrying type: 'college':
api.map.setLayerOverride({
layerId: 'commercial',
sourceLayer: 'landuse',
// the custom tiles tag districts with `type`, campuses as 'college'
filter: ['in', ['get', 'type'], ['literal', ['commercial', 'college']]],
});The fill is semi-transparent, so overlapping district polygons blend. Keep them disjoint.
Set Default Layer Visibility#
setDefaultLayerVisibility#
api.map.setDefaultLayerVisibility(cityCode: string, layerDefaults: Partial<Record<string, boolean>>): voidSet layer visibility defaults for a city. Merged into the player's layer settings when the city loads.
Keys are layer names as used by the Layers panel. The call does not validate them. On city load the defaults are spread over the current layer settings and written to the layer store, logged as [MainStore] Applied layer defaults for <code>: (development builds only). A loaded save that stores layer settings applies its boolean entries for known layers on top. Logs [Modding API] Setting default layer visibility for <code>: (development builds only) with the keys.
| Param | Type | Default | Description |
|---|---|---|---|
cityCode | string | required | plain city code; must be a non-empty string. |
layerDefaults | Partial<Record<string, boolean>> | required | map of layer name to visibility. |
Errors:
cityCodemissing or not a string: logs[Modding API] Invalid city code, stores nothing.layerDefaultsmissing or not an object: logs[Modding API] Invalid layer defaults - must be an object, stores nothing.- exception: logs
[Modding API] Error setting default layer visibility:.
Side effects: layer default registry entry replaced; layer store written on city load.
// applied when HFX loads; a save's own layer settings win
api.map.setDefaultLayerVisibility('HFX', { oceanFoundations: false, roadLabels: true });getDefaultLayerVisibility#
api.map.getDefaultLayerVisibility(cityCode: string): Partial<Record<string, boolean>> | undefinedThe layer visibility defaults stored for a city, or undefined.
Parameter cityCode (string): plain city code.
Returns: Partial<Record<string, boolean>> | undefined. the object passed to setDefaultLayerVisibility (same reference); undefined when none.
Errors: exception: logs [Modding API] Error getting default layer visibility:, returns undefined.
const defaults = api.map.getDefaultLayerVisibility('HFX') || {};
console.log(Object.keys(defaults));Routing Service Override#
setRoutingServiceOverride#
api.map.setRoutingServiceOverride(override: RoutingServiceOverride): voidRegister a driving-directions service for a city. Inside the game only map.queryRoute uses the registry; map.getRoutingServiceOverride returns the stored entry.
The commute simulation does not read the registry. A second call for the same city replaces the entry. Point routingUrl at an http://127.0.0.1:<port> service on the player's machine: in the desktop app the request filter cancels a request whose URL does not match the app's fixed network allowlist of exact hosts, and that allowlist and the content security policy both admit http://127.0.0.1 on any port. Logs [Modding API] Setting routing service override for <code>: <url> (development builds only).
Parameter override (RoutingServiceOverride): city code, URL template, response format and optional parser.
Errors:
- missing
override,cityCodeorroutingUrl: logs[Modding API] Invalid routing service override - must have cityCode and routingUrl, stores nothing. - exception: logs
[Modding API] Error setting routing service override:.
Side effects: routing override registry entry replaced.
api.map.setRoutingServiceOverride({
cityCode: 'HFX',
routingUrl: 'http://127.0.0.1:5000/route/v1/driving/{origin_lon},{origin_lat};{dest_lon},{dest_lat}',
format: 'osrm',
});Custom Parser#
With format: 'custom', queryRoute fetches the URL, parses the JSON and returns customParser(json) unchanged. The parser returns { drivingSeconds, drivingDistance, drivingPath? } in s, m and [lng, lat] pairs. A throw in the parser logs [Modding API] Error querying route: and resolves null.
api.map.setRoutingServiceOverride({
cityCode: 'MTL',
routingUrl: 'http://127.0.0.1:3000/route?from={origin_lon},{origin_lat}&to={dest_lon},{dest_lat}',
format: 'custom',
customParser: (response) => ({
drivingSeconds: response.time_sec,
drivingDistance: response.dist_m,
drivingPath: response.coords, // optional
}),
});getRoutingServiceOverride#
api.map.getRoutingServiceOverride(cityCode: string): RoutingServiceOverride | undefinedThe routing service registered for a city, or undefined.
Parameter cityCode (string): plain city code.
Returns: RoutingServiceOverride | undefined. the object passed to setRoutingServiceOverride (same reference); undefined when none.
Errors: exception: logs [Modding API] Error getting routing service override:, returns undefined.
const routing = api.map.getRoutingServiceOverride('HFX');
console.log(routing ? `HFX routes via ${routing.format}` : 'HFX has no routing service');queryRoute#
api.map.queryRoute(cityCode: string, origin: [number, number], destination: [number, number]): Promise<{ drivingSeconds: number; drivingDistance: number; drivingPath?: [number, number][] } | null>Fetch a driving route from the city's registered routing service and normalise the response.
The URL is built by replacing the first occurrence of each of {origin_lon}, {origin_lat}, {dest_lon} and {dest_lat} in routingUrl, then fetched with fetch and parsed as JSON. osrm reads routes[0].duration (s), routes[0].distance (m) and routes[0].geometry.coordinates. valhalla reads trip.summary.time (s), trip.summary.length times 1000 (km to m) and decodes trip.legs[0].shape (precision-6 polyline). A missing summary field reads as 0. graphhopper reads paths[0].time divided by 1000 (ms to s), paths[0].distance (m) and paths[0].points.coordinates. A missing time or distance reads as 0. custom returns customParser(json) unchanged. In the desktop app a request to a host outside the app's fixed network allowlist is cancelled, so the call logs [Modding API] Error querying route: and resolves null; an http://127.0.0.1:<port> service on the player's machine passes both that allowlist and the content security policy.
| Param | Type | Default | Description |
|---|---|---|---|
cityCode | string | required | plain city code with a registered routing service. |
origin | [number, number] | required | [lng, lat]; degrees. |
destination | [number, number] | required | [lng, lat]; degrees. |
Returns: Promise<{ drivingSeconds: number; drivingDistance: number; drivingPath?: [number, number][] } | null>. { drivingSeconds, drivingDistance, drivingPath? } with seconds, meters and [lng, lat] pairs; null when there is no usable route.
Errors:
- no service for the city: logs
[Modding API] No routing service registered for <code>, resolvesnull. - non-2xx response: logs
[Modding API] Routing request failed: <status>, resolvesnull. - response without a route (
routes[0],triporpaths[0]missing): resolvesnull. format: 'custom'withoutcustomParser: logs[Modding API] Custom format requires customParser, resolvesnull.- unknown
format: logs[Modding API] Unknown routing format: <format>, resolvesnull. - network or parser exception: logs
[Modding API] Error querying route:, resolvesnull.
const r = await api.map.queryRoute('MTL', [-73.5673, 45.5017], [-73.55, 45.52]);
if (r) api.demand.addPops([{ residenceId, jobId, drivingSeconds: r.drivingSeconds, drivingDistance: r.drivingDistance }]);MapLayer#
A MapLibre layer specification for map.registerLayer. Further MapLibre layer properties pass through.
| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Layer id; required. A duplicate replaces the earlier registration. |
type | string | required | MapLibre layer type such as 'raster', 'fill' or 'line'. |
source | string | none | Id of the source the layer draws from. |
MapSource#
A MapLibre source specification for map.registerSource. Further MapLibre source properties pass through.
| Field | Type | Default | Description |
|---|---|---|---|
type | string | required | MapLibre source type such as 'raster', 'vector' or 'geojson'. |
url | string | none | Passed to MapLibre unchanged. |
tiles | string[] | none | Tile URL templates with {z}/{x}/{y} placeholders. |
TileURLOverride#
Per-city tile URL replacement for map.setTileURLOverride.
| Field | Type | Default | Description |
|---|---|---|---|
cityCode | string | required | Plain city code the override applies to; required. |
tilesUrl | string or string[] | none | URL template(s) for the general tiles with {z}/{x}/{y}. A string is wrapped in an array. The array becomes the vector source's tiles list. |
foundationTilesUrl | string or string[] | none | URL template(s) for the building foundation tiles with {z}/{x}/{y}. A string is wrapped in an array. The array becomes the foundation source's tiles list while building foundations are shown. |
maxZoom | number | 15 | Source maxzoom for both tile sources; zoom levels. |
LayerSourceOverride#
Basemap layer override for map.setLayerOverride.
| Field | Type | Default | Description |
|---|---|---|---|
layerId | string | required | Id of a map layer; required. filter and paint apply at once to any loaded layer with this id; when it builds the style, the style builder reads the stored override for water, parks-large, parks-small, commercial and airports (and only its filter for buildings-3d). |
sourceLayer | string | none | Replacement source-layer; applied at the next style build; ignored for buildings-3d. |
filter | any[] | none | Replacement MapLibre filter expression; applied live and at style build; combined with the demolished-buildings filter for buildings-3d. |
paint | Record<string, any> | none | Paint properties merged over the layer's defaults; applied live and at style build; the style builder ignores it for buildings-3d. |
RoutingServiceOverride#
Driving-directions service for map.setRoutingServiceOverride and map.queryRoute.
| Field | Type | Default | Description |
|---|---|---|---|
cityCode | string | required | Plain city code the service serves; required. |
routingUrl | string | required | URL template with {origin_lon}, {origin_lat}, {dest_lon}, {dest_lat} placeholders (first occurrence of each is replaced); required. |
format | 'osrm' or 'valhalla' or 'graphhopper' or 'custom' | required | Response format: 'osrm', 'valhalla', 'graphhopper' or 'custom'. Selects the parser in queryRoute. |
customParser | (response: any) => { drivingSeconds: number; drivingDistance: number; drivingPath?: [number, number][]; } | none | Parser for format: 'custom'; receives the parsed JSON and returns { drivingSeconds, drivingDistance, drivingPath? } in s, m and [lng, lat] pairs. |
Example: Custom Basemap#
registerStyle changes nothing on the map, so a basemap is a raster source plus a layer. The desktop app cancels requests to hosts outside its allowlist, which admits plain http:// on 127.0.0.1 and localhost: serve the tiles from the player's machine. beforeId keeps the game's road labels above the basemap; registered sources and layers come back after every style reload.
const api = window.SubwayBuilderAPI;
api.map.registerSource('my-basemap-tiles', {
type: 'raster',
tiles: ['http://127.0.0.1:8080/basemap/{z}/{x}/{y}.png'],
tileSize: 256,
attribution: '© OpenStreetMap contributors',
});
api.map.registerLayer(
{ id: 'my-basemap', type: 'raster', source: 'my-basemap-tiles', paint: { 'raster-opacity': 0.8 } },
'road-labels'
);