Reachable ranges module
ReachableRangesModule draws the areas reachable from one or more origins within a budget of time or distance,
as calculateReachableRanges returns them. Each range is one band,
titled from its budget, nested around its own origin and coloured from the map. Calculating the ranges needs
an API key with Calculate Reachable Range access, which is in private preview. The module draws them on any map.
- It owns the bands, their borders and the labels along them. It also owns how they look, through the fill style, palette, labels and highlight below.
- It never restyles the base map, which belongs to the Styling Foundations module, and styling
knobs never reach its layers. Bands with no
paletteread the map’s accent and land colours. Who owns what draws the line for every module.
The demo shows 10, 20 and 30 minutes of driving from two stations, with every module setting on its panel:
Showing ranges
import { TomTomConfig } from '@tomtom-org/maps-sdk/core';import { ReachableRangesModule, TomTomMap } from '@tomtom-org/maps-sdk/map';import { calculateReachableRanges } from '@tomtom-org/maps-sdk/services';
TomTomConfig.instance.put({ apiKey: 'YOUR_API_KEY' });const origin: [number, number] = [4.9041, 52.3676];const map = new TomTomMap({ mapLibre: { container: 'map', center: origin, zoom: 9 } });const rangesModule = await ReachableRangesModule.create(map);
await rangesModule.show( await calculateReachableRanges([10, 20, 30].map((value) => ({ origin, budget: { type: 'timeMinutes', value } }))),);show replaces the ranges shown, and clear removes them. The module draws what calculateReachableRanges
returns unchanged:
- The module groups ranges by their
origin, so oneshowand one request can hold the bands of several places, each nested around its own. - Ranges can come in any order. A smaller range always draws over the larger ones around it.
- Each band’s border is labelled from its budget, as Titles describes.
const origins: [number, number][] = [[4.9003, 52.3791], [5.1109, 52.0894]];await rangesModule.show( await calculateReachableRanges( origins.flatMap((origin) => [10, 20, 30].map((value) => ({ origin, budget: { type: 'timeMinutes', value } }))), ),);Titles
A range’s title is the text of the labels along its border. By default, it is the range’s budget with its unit,
such as 10 min or 5 km, otherwise its position in what show was given, from 1. A
lineLabel.title function of your own replaces it, and changing it relabels the bands shown:
rangesModule.updateConfig({ lineLabel: { title: (feature, index) => `Zone ${index + 1}` } });lineLabelalso takes the styling fields every module’s labels share:size,color,haloColor,haloWidth,opacityandfont.- If you don’t set the colours, they follow the map style. Light styles get dark text on a white halo, and dark styles the reverse.
updateConfigreplaces the wholelineLabel, so spread the current one to keep a title, or set one field withsetKnob:
rangesModule.updateConfig({ lineLabel: { ...rangesModule.getConfig()?.lineLabel, size: 13, color: '#1d3f8a' } });Fill styles
fill.style | Draws |
|---|---|
'filled' (default) | each band coloured, the innermost on top |
'outline' | only each range’s border, thick and coloured |
'inverted' | what lies out of reach: the world beyond the largest range of every origin, then a ring outside each smaller one |
const outOfReach = await ReachableRangesModule.create(map, { fill: { style: 'inverted' } });These are the geometries module’s fill styles, applied per band.
Band colours
By default, the bands ramp from the map’s
accent on the innermost band
toward its land colour on the outermost, both read from map.mapColors.
'inverted'ramps the other way, deepening away from the origin.- On a map with no theme, the ramp follows the style’s light or dark side. It runs from navy
#0A3653on light styles, or sky blue#5FA8D8on dark ones, toward the style’s land colour. Before a styling foundations module publishes the map colours, it runs toward white or#333333instead. It repaints when you switch. - If you theme the map, for example with the map-theme plugin, the bands repaint with it:
import { StylingFoundationsModule } from '@tomtom-org/maps-sdk/map';import { deriveMapColors } from '@tomtom-org/maps-sdk-plugin-map-theme';
const styling = await StylingFoundationsModule.get(map);styling.setMapColors(deriveMapColors(['#1d3f8a', '#e63312', '#f5d000', '#f4f1ea'])); // the bands followfill.palette sets colours of your own instead, innermost band first, and starts over after the last. Map
colour changes don’t touch them, and an empty list hands the colours back to the map. updateConfig replaces
the whole fill, so spread the current one to keep its style:
rangesModule.applyConfig({ fill: { palette: ['#b3001b', '#e85d04', '#faa307'] } });rangesModule.updateConfig({ fill: { ...rangesModule.getConfig()?.fill, palette: [] } });Bands are ranked per origin, so with several origins the ranges of the same rank share a colour.
To shade the bands from your first colour to your last, set fill.paletteMode from 'cycle' to 'ramp'. Two colours then
cover any number of bands, blended in the OKLCH colour space so the steps between them stay vivid:
rangesModule.applyConfig({ fill: { palette: ['#f5d000', '#6a1b9a'], paletteMode: 'ramp' } });To fade the bands out away from the origin, set fill.outermostOpacityFactor below 1. It multiplies the
outermost band’s fill opacity. The innermost band keeps the full opacity, and the bands between step evenly. With
'inverted', the world beyond the largest range is the outermost band.
rangesModule.updateConfig({ fill: { ...rangesModule.getConfig()?.fill, outermostOpacityFactor: 0.2 } });Hovered and clicked bands
Once a handler covers the bands, the band under the pointer draws a denser fill and a wider border in its own colour. So does a clicked band. The module’s event state methods set that state from your own UI.
highlightsets how much. By default, the fill opacity doubles, up to fully opaque, and the border width grows 1.75 times.- A factor of
1draws a hovered band like any other.
rangesModule.updateConfig({ highlight: { fill: { opacityFactor: 1 }, line: { widthFactor: 2.5 } } }); // a bolder border aloneEvents
events covers the bands and their labels. A handler receives the band as drawn, with the title and color
the module gave it in its properties:
rangesModule.events.on('click', (band) => console.log(band.properties.title)); // '20 min'Two named scopes each cover one surface, with the same handlers and event states.
events.geometry covers the bands, their borders and the labels along them, which are drawn from the same
features. events.geometryLabel covers the title points getShown() lists under geometryLabel.
rangesModule.events.geometry.on('hover', (band) => showTooltip(band.properties.title));The user events guide covers the event types and their timings.
Showing and hiding
setVisible hides and shows the bands, borders and labels together, and visible: false at creation starts the
module hidden. show() and clear() never change it, so ranges shown while hidden appear on setVisible(true).
The state survives a style change.
rangesModule.setVisible(!rangesModule.isVisible());Layer position
The bands sit below the map’s labels unless beforeLayerConfig says otherwise. A single target places the fill
and the borders together, and the record form { all?, fill?, line? } places them separately, as on the
geometries module:
const ranges = await ReachableRangesModule.create(map, { beforeLayerConfig: { fill: 'lowestRoadLine', line: 'lowestLabel' }, // fill under the roads});Dashed and dotted borders
To break the borders up, set line.pattern to 'dashed' or 'dotted'. The dashes and dots scale with the
border’s width, in every fill style:
rangesModule.updateConfig({ line: { pattern: 'dotted' } });Layer overrides
For anything the settings don’t cover, layers takes a partial MapLibre layer for the bands (fill), their
borders (line) and labels (label, lineLabel), the same entries as
the geometries module’s. paint and layout merge per property
onto the module’s own, in every fill style:
ranges.updateConfig({ layers: { line: { paint: { 'line-blur': 4 } } } });Restyling and saving the settings
A config change restyles the bands already shown, without calculating the ranges again. updateConfig changes
only what you give it, applyConfig replaces the whole config, and resetConfig returns to the defaults.
getConfig() returns the settings to store. All of it survives JSON except the function lineLabel.title.
ReachableRangesModuleSerializableConfig types what you parse back, and on replay you add lineLabel.title in code:
import type { ReachableRangesModuleSerializableConfig } from '@tomtom-org/maps-sdk/map';
localStorage.setItem('ranges', JSON.stringify(rangesModule.getConfig()));
const saved: ReachableRangesModuleSerializableConfig = JSON.parse(localStorage.getItem('ranges') ?? '{}');rangesModule.applyConfig({ ...saved, lineLabel: { ...saved.lineLabel, title: (feature, index) => `Zone ${index + 1}` } });Every setting as data
reachableRangesKnobCatalogue lists every plain-valued setting of the module with its kind, its range or options
and, where one value holds everywhere, its default. Each id is the setting’s path in the config, so a panel can
build its controls from it and set each with setKnob:
import { reachableRangesKnobCatalogue, setKnob } from '@tomtom-org/maps-sdk/map';
setKnob(rangesModule, reachableRangesKnobCatalogue, 'lineLabel.size', 13);| Id | Kind |
|---|---|
visible, fill.style, fill.palette, fill.paletteMode | toggle, enum, colours, enum |
fill.outermostOpacityFactor | factor, 0 to 1, 1 by default |
line.pattern | enum: solid (default), dashed, dotted |
highlight.fill.opacityFactor, highlight.line.widthFactor | factor, 1 to 4 and 1 to 3 |
lineLabel.size, lineLabel.color, lineLabel.haloColor, lineLabel.haloWidth, lineLabel.opacity, lineLabel.font, lineLabel.typeface | number, colour, colour, number, number, enums, enum |
- The label knobs default like the geometries module’s border labels: size 15, halo width 2 and opacity 1.
Unset, the font is
Noto-Boldset in the labels’ typeface. reachableRangesKnobIdslists the ids in catalogue order,ReachableRangesKnobIdis their union, andReachableRangesKnobValueOf<ID>is the plain value one takes.- The catalogue leaves out
events, andbeforeLayerConfig,layersand the labels’titleandcustomTypeface, which take structured, MapLibre or function values. - Its entries share their shape,
KnobEntry, with every module’s catalogue.
Reading shown data
getShown() returns what the module draws, as prepared for display:
geometry- The bands with their titles and colours. Withfill.style: 'inverted', it holds the shaded rings and a copy of each range for its border.geometryLabel- A title point derived from each band. The labels along the borders are drawn fromgeometryitself.
const { geometry } = rangesModule.getShown();console.log(`Bands: ${geometry.features.length}`);Multiple module instances
The module is data-owned, so each instance has its own sources and layers. Two can show two sets of
ranges side by side, apart from any GeometriesModule on the same map.
const byCar = await ReachableRangesModule.create(map);const byBike = await ReachableRangesModule.create(map, { fill: { style: 'outline' } });Related guides and examples
Related guides
- Reachable ranges - Calculating the ranges: budget types, vehicles, ranges that can’t be calculated and cancelling a batch
- Geometries module - Polygons of your own, styled per feature rather than as bands
- Map theme plugin - The map colours the bands follow
Related examples
- Reachable ranges playground - Every knob of the module, and bands following a map theme
- Reachable ranges - Ranges around the clicked point, by time or distance
- Routing composite - Ranges alongside a route and its places
Related map modules
- User interaction events - Handling clicks and hovers on the bands