Map modules
A map module adds one capability to a TomTomMap: showing places or routes, filtering traffic, shading terrain, restyling the whole map. Every module shares one contract, which this page owns:
- Two kinds, by who owns the layers: style-owned or data-owned, which decides the factory and how many instances a map takes.
- One configuration surface: the configuration given at creation, then
applyConfig(config),updateConfig(partial)andresetConfig(), read back withgetConfig()and reported byevents.on('config-change', …). See Changing a setting. - Settings as data: each module publishes a knob catalogue, and
setKnobsets any setting by its id. - Restored on style changes: data, configuration and layer placement come back after every
setStyle. - Typed user events per module, handed your original data: user interaction events.
- Tree-shaken: a module you do not import is not in your bundle.
Types of modules
Who owns the sources and layers a module controls decides both the factory you call and how many instances make sense on one map.
Style-owned modules — get(map, config?)
- They control sources and layers the map style already provides — the base map, POIs, the traffic tiles, the terrain elevation — under fixed, global ids, and display no data of their own.
- One per map:
get()is a lookup, so a second call returns the same instance. Two components can each ask for the terrain without sharing a reference. get(map)without a configuration leaves the module's configuration as it is; with one, it applies it to the existing instance.- The traffic and terrain modules'
get()adds their style part when the style lacks it. - To handle events on part of what one covers, scope its events rather than looking for a second module.
Data-owned modules — create(map, config?)
- They add the GeoJSON sources, layers and sprite images they need, and own them for their whole lifetime, every id suffixed per instance.
- Many per map:
create()builds a new, independent module every time — one set of places per search, one route per plan.
Per-module reference
| Module | Kind | What it draws or controls |
|---|---|---|
| BaseMapModule | Style-owned | Which base-map layer groups and road parts show, and the events on them |
| POIsModule | Style-owned | The style's POIs: visibility, category filters, size, labels, category icons |
| TrafficFlowModule | Style-owned | The live traffic flow tiles |
| TrafficIncidentsModule | Style-owned | The live traffic incident tiles |
| TerrainModule | Style-owned | Hillshade shading and the 3D surface |
| StylingFoundationsModule | Style-owned | Sizes, map colours, projection and sky across the whole map |
| PlacesModule | Data-owned | Places you give it, as pins, circles or base-map-looking icons |
| RoutingModule | Data-owned | Routes, waypoints and route sections |
| GeometriesModule | Data-owned | Polygons and lines, such as administrative areas |
| ReachableRangesModule | Data-owned | The bands calculateReachableRanges returns |
| CustomGeoJSONModule | Data-owned | Your own GeoJSON, with your own MapLibre layer specs |
| TrafficIncidentDetailsModule | Data-owned | Incidents you fetched with trafficIncidentDetails |
| TrafficAreaAnalyticsModule | Data-owned | Area analytics as a hexgrid, square grid or heatmap |
Who owns what
Everything visible on the map has exactly one owner, and the owner follows from who owns the layers. Every module has knobs of its own: each style-owned module decides whether and how its part of the loaded style shows, the Styling Foundations module the foundations that reach across every part — sizes, the map colours, projection and sky — and each data-owned instance everything about the layers it adds itself. The dashed line is the only place a knob reaches past its module: the data-owned modules read the accent as their default colour.
In detail:
| You want to change… | Owner | How |
|---|---|---|
| Whether something the loaded style draws is shown — a base-map layer group or road part, the traffic tiles, a POI category, the hillshade, the 3D surface | The style-owned module for that part | setVisible, updateConfig({ filters }), setHillshadeVisible, setElevationVisible, setKnob |
| How one part looks — POI size and label colours, traffic colours and widths, hillshade lighting | The style-owned module for that part: POIs, traffic flow, traffic incidents, terrain | setKnob, updateConfig |
| The foundations across every part — label, icon and road sizes, the ten map colours, projection and sky | The Styling Foundations module | setKnob, setMapColors |
| A named bundle of looks across those modules | applyMapPreset |
applyMapPreset(map, id) |
| Anything a data-owned module draws itself — routes, places, geometries, your GeoJSON, fetched traffic incidents, area analytics | That module, through its own configuration | create(map, config), applyConfig / updateConfig, setKnob and setVisible |
| The map's colour identity — the accent routes, places and geometries default to | The Styling Foundations module publishes it; each data-owned module reads it | setMapColors, the colors.accent knob; a colour set on the module always wins |
| A look a data-owned module borrows from the style, such as POI-looking places or the colours of fetched traffic incidents | The module whose knob restyles it sets it on the style; the data-owned module reads it and redraws its own layers | The knobs that restyle what it borrows; a setting given to the module always wins |
Two consequences:
- The rows do not cross. Knobs on the loaded style never write a data-owned module's layers, and a data-owned module never restyles the base map under it: recolouring a route is
RoutingModuleconfiguration, recolouring the roads it runs on iscolors.roadMajor. The accent and the borrowed looks are read, not written. - The foundations and a part's own look meet on the same layers, and compose: one engine per map applies every style knob, whichever module holds it, so
labels.sizeFactorand the POIs'sizeFactormultiply on the POI labels, and all of them land on top after a style switch.
Within a style-owned module, the finer settings only narrow what its coarser ones show. A road part or the POI micro markers never show while their layer group or the POIs are hidden, and showing those again keeps a part's false; the POIs' zoomShift rewrites the same layer filters filters.categories narrows, in either order.
Each module guide opens with a What this module owns line that applies this rule to that module.
Changing a setting
Every setting changes through three calls each module has: applyConfig(config) replaces the whole configuration, updateConfig(partial) changes the top-level properties it is given, and setKnob sets one setting by its catalogue id.
updateConfigmerges one level deep. A nested object it is given replaces the one there, so to change one nested field, spread the current part or set its knob:import { placesKnobCatalogue, setKnob } from '@tomtom-org/maps-sdk/map'; placesModule.updateConfig({ label: { ...placesModule.getConfig()?.label, color: '#c2185b' } }); setKnob(placesModule, placesKnobCatalogue, 'label.color', '#c2185b');Named setters are for visibility:
setVisible/isVisibleand the parts that hide on their own (see Visibility under Appearance vocabulary). The only others areTrafficAreaAnalyticsModule's per-metricsetPalette,setColorStops,setHeightandsetFilters. A named setter is alwaysset<Setting>.
Saving and replaying a configuration
getConfig() is the export and applyConfig() the import: a module's configuration that went through JSON, given back to applyConfig, holds the same configuration and draws the same layers. Keep one to restore a user's map, attach it to a support report, or replay it in a test.
localStorage.setItem('places', JSON.stringify(placesModule.getConfig()));
// Later, on the same module or a new one
placesModule.applyConfig(JSON.parse(localStorage.getItem('places')));
Most modules' configurations are JSON whole, styling's knobs included. Five modules also take functions or image elements, which JSON cannot carry. Each has a type for the part that survives, which its reference page details; a replay adds the rest back in code:
| Module | Serializable part | Left out |
|---|---|---|
PlacesModule |
PlacesModuleSerializableConfig |
label.title as a function, connections.label.title, evAvailability.formatText, icon.mapping, extraFeatureProps functions, image elements in icon |
RoutingModule |
RoutingModuleSerializableConfig |
a custom chargingStops.icon.mapping, waypoints.icon.mapping, waypoints.label.title as a function, image elements in chargingStops.icon and waypoints.icon |
GeometriesModule |
GeometriesModuleSerializableConfig |
transformFeaturesForDisplay |
ReachableRangesModule |
ReachableRangesModuleSerializableConfig |
lineLabel.title |
CustomGeoJSONModule |
CustomGeoJSONModuleSerializableConfig |
images |
JSON.stringify drops a function without a trace, but turns an image element into {}, which does not replay. Give icon images as URLs, data URIs or SVG text, and a place or waypoint title as a MapLibre expression, to keep the whole configuration serializable. POIsModule's icon.categoryIcons takes image elements too, with no type to leave them out.
const state = JSON.parse(saved); // PlacesModuleSerializableConfig
placesModule.applyConfig({
...state,
connections: { ...state.connections, label: { ...state.connections?.label, title: (connection) => connection.name } },
});
The configuration is what a module draws with, not the data it shows: replay show() with the data too.
Appearance vocabulary
A concept has one name on every module that has it. The table lists the shared appearance terms, the rule each follows, and the exact properties that carry it; a module missing from a row does not have that concept.
| Concept | The rule | Modules and properties |
|---|---|---|
| The module's colour | A top-level color is the colour of what the module draws. Unset, it is the map's accent (colors.accent, published as map.mapColors.accent), then the module's own default. A part-level colour overrides it. |
RoutingModule: color, under layers.* paint overrides · GeometriesModule: color, under fill.palette, fill.color, line.color and a feature's color property (order) · PlacesModule: color, under icon.default.style.fillColor, label.color and layers.* |
| Labels | One label shape, LabelConfig: size, color, haloColor, haloWidth, opacity, font. Each module adds the label's content and its own offset; FeatureLabelConfig adds a title, a function or a MapLibre expression. |
PlacesModule: label (PlaceLabelConfig, adds title, and offset as one number of ems away from the icon) · RoutingModule: waypoints.label (WaypointLabelConfig, adds title and visible), and chargingStops.label (ChargingStopLabelConfig: visible and a title expression, without the LabelConfig styling) · GeometriesModule: label (GeometryLabelConfig, adds text, a string or expression that defaults to the feature's title), and lineLabel (GeometryLineLabelConfig, the same text plus minZoom, symbolSpacing, and offset as [x, y] ems) · ReachableRangesModule: lineLabel (ReachableRangeLineLabelConfig, adds a title function) |
| Multipliers | A *Factor multiplies a default and is clamped to its catalogue entry's range, which knobEntryOf(catalogue, id) reads. Where a size also has presets ('s', 'm', 'l'), the preset picks the base and the factor multiplies it. |
RoutingModule: width and widthFactor (0.5 to 2), waypoints.size and waypoints.sizeFactor (0.5 to 2), sections.<type>.width and sections.<type>.widthFactor, sections.<type>.icon.sizeFactor (0.25 to 2), highlight.outline.widthFactor (1 to 1.25), waypoints.highlight.sizeFactor (1 to 1.5) · GeometriesModule: highlight.fill.opacityFactor (1 to 4) and highlight.line.widthFactor (1 to 3) · TrafficIncidentDetailsModule: highlight.line.widthFactor (1 to 1.8) · POIsModule: sizeFactor (0.5 to 1.5) · TrafficFlowModule and TrafficIncidentsModule: widthFactor (0.5 to 2) · Styling Foundations knobs: labels.sizeFactor, symbols.sizeFactor, roads.widthFactor (0.5 to 1.5) |
| Hovered and clicked look | A feature a handler covers, or one setEventState puts a state on, draws one look whether hovered or clicked. highlight holds the factors that look multiplies by; a factor of 1 draws the feature as any other. A MapLibre layer you give in place of the default draws as given. |
RoutingModule: highlight.outline.widthFactor, waypoints.highlight.sizeFactor · GeometriesModule: highlight.fill.opacityFactor, highlight.line.widthFactor · TrafficIncidentDetailsModule: highlight.outline.color, highlight.line.widthFactor, also drawn on the incidents setFocus marks, highlight: false to turn it off · PlacesModule: layers.selected |
| Absolute units | A value that mirrors a MapLibre property keeps MapLibre's name and is in pixels. Any other unit is in the name. Opacities run from 0 to 1, label offsets are in ems. | Pixels: LabelConfig size and haloWidth, GeometriesModule line.width and lineLabel.symbolSpacing, PlacesModule cluster.badge.outlineWidth and cluster.badge.radius · Metres: TrafficAreaAnalyticsModule maxHeightMeters, minHeightMeters and, per unit of the metric, metersPerUnit · Two exaggerations: TerrainModule elevation.exaggeration scales the raised surface, its hillshade.exaggeration sets the shading strength |
| Marker type, fill style, map theme | Theme means only the map-wide look. How one module draws its own features has a name of its own. | PlacesModule: markerType (PlacesMarkerType; placesMarkerTypes lists pin, circle-icon, base-map, pin-clustered) · GeometriesModule: fillStyle (GeometryFillStyle; geometryFillStyles lists filled, outline, inverted), per feature properties.fillStyle · ReachableRangesModule: fillStyle, the same values · Map theme: StylingFoundationsModule.setMapColors, fed by the Map Theme plugin's deriveMapColors, deriveMapColorsFromCss, deriveMapColorsFromImage and mapColorsLightDark |
| Fill colour or palette | A colour field takes a colour or a MapLibre expression, never a palette name. A palette is its own field: on geometries and reachable ranges a list of your own CSS colours, which the features or bands take one after another; on area analytics a name, ramped over the metric's value. | GeometriesModule: fill.color, fill.palette, unset drawing one colour, the accent · ReachableRangesModule: fill.palette, unset ramping from the accent toward the map's land colour · TrafficAreaAnalyticsModule: metrics.<metric>.palette (AreaAnalyticsPalette; areaAnalyticsPalettes lists the names), and metrics.<metric>.colorStops, which wins over it |
| Zoom thresholds | minZoom and maxZoom. |
PlacesModule: entryPoints.minZoom · GeometriesModule: lineLabel.minZoom · RoutingModule: sections.<type>.sign.minZoom, countryCrossings.minZoom · POIsModule: minZoom · TerrainModule: hillshade.maxZoom |
| Borrowed looks | A part drawn to look like the style follows the knobs that restyle what it borrows. A look set on the module wins. | PlacesModule with markerType base-map, circle-icon or pin-clustered · RoutingModule sections.traffic · TrafficIncidentDetailsModule line colours. The knobs each one follows: Borrowed looks |
| Visibility | visible in the config, setVisible(visible) to change it, isVisible() to read it back. isVisible() returns the setting, the same value as the module's visible knob. On a data-owned module, hiding keeps the data, show() and clear() leave it hidden, and getShown() returns what is shown. On a style-owned module that nothing has set, isVisible() reads what the style draws, and the traffic modules are hidden until shown; getRenderedFeatures() returns what its tiles render in view. A part that hides on its own has its own visible. |
All three on PlacesModule, RoutingModule, GeometriesModule, ReachableRangesModule, CustomGeoJSONModule, TrafficIncidentDetailsModule, TrafficAreaAnalyticsModule, POIsModule, TrafficFlowModule and TrafficIncidentsModule (plus icons.visible with setIconsVisible / isIconsVisible) · BaseMapModule: visible, groups.<group>.visible, setVisible and isVisible with { layerGroups } · RoutingModule per part: chargingStops.visible, chargingStops.label.visible, summaryBubbles.visible, countryCrossings.visible, sections.<type>.visible · TerrainModule: hillshade with setHillshadeVisible / isHillshadeVisible, elevation with setElevationVisible / isElevationVisible |
| Layer position | beforeLayerConfig takes 'top' or a named style layer such as 'lowestLabel' (BeforeLayerConfig) for all of a module's layers. A module drawing several parts also takes a record keyed by its part names, all covering the parts it leaves out (PartsBeforeLayerConfig). |
GeometriesModule and ReachableRangesModule: GeometryBeforeLayerConfig, parts fill, line · RoutingModule: RoutingBeforeLayerConfig, parts mainLines, instructionLines, icons · PlacesModule: PlacesBeforeLayerConfig, parts places, connections · TrafficAreaAnalyticsModule: AreaAnalyticsBeforeLayerConfig, one part per display mode · TrafficIncidentDetailsModule: BeforeLayerConfig alone · CustomGeoJSONModule: CustomGeoJSONBeforeLayerConfig, for all its layers and on each layer, which also takes { layerID } to go below a layer outside the style |
| Value filters | One filter shape, ValuesFilter: { show, values }, where show is one of filterShowModes, 'only' (just the values) or 'all-except' (everything but them). |
POIsModule: filters.categories · TrafficFlowModule and TrafficIncidentsModule: filters fields such as roadCategories and, on incidents, magnitudes · BaseMapModule: the layerGroups that setVisible, isVisible and events.where take |
Every setting as data
A module's display settings are also published as a knob catalogue: static data, readable with no map loaded, that a control panel builds its controls from and an agent checks a value against. An entry's id is the setting's path in the module's configuration, where a number indexes a list (filters.any.0), and its kind says which value it takes and which fields come with it. The entry type is KnobEntry, and the kinds are KnobKind:
| Kind | Value | Fields |
|---|---|---|
toggle |
true or false |
|
factor |
a multiplier, 1 keeping what the style does |
range |
number |
a number | range |
offset |
an [x, y] displacement, x to the right and y down, in the unit the description names |
range, for x and y each |
color |
a CSS colour | |
enum |
one of the options | options |
enums |
a list of the options; the description says when their order matters | options |
colors |
a list of CSS colours or of the names in options |
options, where there are names |
image |
the id of an image in the style's sprite | |
text |
any string |
rangeis the span a control offers, with thestepa slider uses. Itsboundssay which ends a value must keep to:boundsA value must be For example hardbetween minandmaxan opacity, a *Factorhard-minat least min; abovemaxit still appliesa height in metres softany number an offset defaultis present where one value holds everywhere. Without it, the description says what decides: the style, the accent, the global configuration.A setting that takes a MapLibre expression, a callback or a structured object stays in the configuration. The plain-value alternatives of a setting that also takes an expression are in its entry, and they are what the module's
…KnobValueOf<ID>type names.The styling catalogue and the map effects plugin's use the same kinds.
Setting a knob by its id
Three functions work a knob by its catalogue id, on every module, the Styling Foundations module and the map effects plugin's MapEffects included:
setKnob(module, catalogue, id, value)sets one setting and keeps every other one; a value its entry refuses changes nothing.getKnob(module, catalogue, id)reads the value in force: the one set, else what the module knows it to be (a look knob of a style-owned module reads the loaded style), else the entry'sdefault.resetKnob(module, catalogue, id)unsets one, and drops the objects it leaves empty, so a knob set and then reset leaves the configuration as it was.resetConfig()unsets them all.knobEntryOf(catalogue, id)reads one entry: kind, range or options, default and description.validateKnobValue(entry, value)is the check alone, for a value from a form or a language model.validateKnobValueandsetKnobthrow aRangeErrornaming the knob and what it expects; the four that take an id throw anErrorfor one the catalogue lacks.A literal id types the value it takes; an id held as any
string, as in a panel looping over a catalogue, takes any value and is checked when set.
The path is the same for every module, and a knob only ever redraws what its own module draws:
import { getKnob, knobEntryOf, resetKnob, setKnob, trafficFlowKnobCatalogue, validateKnobValue } from '@tomtom-org/maps-sdk/map';
setKnob(trafficFlow, trafficFlowKnobCatalogue, 'filters.roadCategories.values', ['motorway', 'trunk']);
getKnob(trafficFlow, trafficFlowKnobCatalogue, 'visible'); // false until set: the entry's default
resetKnob(trafficFlow, trafficFlowKnobCatalogue, 'filters.roadCategories.values');
const knob = knobEntryOf(trafficFlowKnobCatalogue, 'filters.roadCategories.values'); // kind, range or options, default
validateKnobValue(knob, ['motorway', 'cart track']); // RangeError: … 'cart track' is not
The traffic config playground sets every flow and incidents knob this way, from controls built off the two catalogues.