Traffic Incident Overlay Module

The Traffic Incident Overlay Module renders incidents fetched from the trafficIncidentDetails() REST service as a developer-controlled overlay. Use this module when you need typed incident objects in your app code — for popups, list views, route correlation, or interactive focus — and want to control exactly when incidents render.

import { bboxFromGeoJSON, TomTomConfig } from '@tomtom-org/maps-sdk/core';
import { BaseMapModule, TomTomMap, TrafficIncidentOverlayModule } from '@tomtom-org/maps-sdk/map';
import { geocodeOne, trafficIncidentDetails } from '@tomtom-org/maps-sdk/services';
import type { LngLatBoundsLike } from 'maplibre-gl';
import './style.css';
import { API_KEY } from './config';
import { buildPopupHTML, createIncidentPopup } from './popup';

TomTomConfig.instance.put({ apiKey: API_KEY, language: 'en-GB' });

const INITIAL_QUERY = 'London';

(async () => {
    const searchBox = document.getElementById('sdk-example-searchBox') as HTMLInputElement;
    const searchButton = document.getElementById('sdk-example-searchButton') as HTMLButtonElement;
    const statusElement = document.getElementById('sdk-example-status') as HTMLDivElement;

    const map = new TomTomMap({
        mapLibre: {
            container: 'sdk-map',
            center: [-0.13, 51.51],
            zoom: 11,
        },
    });

    const overlay = await TrafficIncidentOverlayModule.get(map);
    const baseMap = await BaseMapModule.get(map);
    const popup = createIncidentPopup();

    overlay.events.on('click', (incident, lngLat) => {
        if (incident.properties.id) {
            overlay.setFocus([incident.properties.id]);
        }
        popup.setHTML(buildPopupHTML(incident)).setLngLat(lngLat).addTo(map.mapLibreMap);
    });

    // Clear focus + popup when the user clicks the basemap (i.e. outside any incident).
    baseMap.events.on('click', () => {
        overlay.setFocus(null);
        popup.remove();
    });

    const setStatus = (text: string, state?: 'error') => {
        statusElement.textContent = text;
        if (state) {
            statusElement.dataset.state = state;
        } else {
            delete statusElement.dataset.state;
        }
    };

    const renderIncidentsFor = async (query: string) => {
        setStatus(`Searching '${query}'…`);
        popup.remove();
        try {
            const place = await geocodeOne(query);
            const result = await trafficIncidentDetails({
                bbox: place,
                timeValidityFilter: ['present'],
            });
            await overlay.show(result);
            map.mapLibreMap.fitBounds(bboxFromGeoJSON(place) as LngLatBoundsLike, {
                padding: 60,
                duration: 600,
            });
            const placeName = place.properties.address.freeformAddress ?? query;
            setStatus(
                result.features.length === 0
                    ? `No current incidents in ${placeName}.`
                    : `${result.features.length} incident${result.features.length === 1 ? '' : 's'} in ${placeName}. Click one for details.`,
            );
        } catch (error) {
            setStatus(error instanceof Error ? error.message : 'Search failed', 'error');
        }
    };

    const triggerSearch = () => {
        const query = searchBox.value.trim();
        if (query) {
            void renderIncidentsFor(query);
        }
    };

    searchButton.addEventListener('click', triggerSearch);
    searchBox.addEventListener('keypress', (event) => {
        if (event.key === 'Enter') triggerSearch();
    });

    searchBox.value = INITIAL_QUERY;
    void renderIncidentsFor(INITIAL_QUERY);
})();

Looking for something else?

  • The default vector-tile Traffic Incidents Module is the recommended default. It auto-renders all incidents on the map, with full road-class-aware styling and declutter.
  • Use the overlay module when your application already fetches incidents via the REST service and you want the on-map rendering to mirror that exact set — no more, no less.

When to use the overlay vs TrafficIncidentsModule

You need…Use
Live, full-coverage incidents with no fetchTrafficIncidentsModule
Typed incident objects in your app codeTrafficIncidentOverlayModule
Render exactly the result of a single REST callTrafficIncidentOverlayModule
Per-feature focus / highlight a subsetTrafficIncidentOverlayModule
Maximum visual fidelity (per-road-class styling, declutter)TrafficIncidentsModule

The vector-tile incidents are hidden in the default style, so the overlay does not collide with them out of the box. If you’ve explicitly enabled TrafficIncidentsModule elsewhere in your app, hide it again before using the overlay to avoid rendering the same incidents twice.

Initialization

import { TomTomMap, TrafficIncidentOverlayModule } from '@tomtom-org/maps-sdk/map';
const map = new TomTomMap({ mapLibre: { container: 'map' } });
const overlay = await TrafficIncidentOverlayModule.get(map);

Rendering incidents

Fetch incidents with the service, then pass the result to show():

import { trafficIncidentDetails } from '@tomtom-org/maps-sdk/services';
const result = await trafficIncidentDetails({
bbox: [4.85, 52.34, 4.95, 52.40],
timeValidityFilter: ['present'],
});
await overlay.show(result);

show() replaces any previously rendered snapshot. Call clear() to remove all incidents.

Focusing a subset

setFocus(ids) writes MapLibre feature-state on each rendered incident. The default visual treatment widens focused incidents and paints a black outline beneath them; unfocused incidents are unchanged — the focus treatment adds emphasis, it does not dim the rest of the overlay. Pass null to clear focus.

overlay.setFocus(['incident-id-1', 'incident-id-2']);
overlay.setFocus(null); // clear

Customising or disabling the focus treatment

The default visual is opinionated but not baked in — pass a focus config to TrafficIncidentOverlayModule.get(map, …):

// Override individual fields (the others fall back to defaults).
const overlay = await TrafficIncidentOverlayModule.get(map, {
focus: { outlineColor: '#1976d2', widthScale: 2 },
});
// Disable the visual entirely. setFocus() still writes
// `feature-state.focused`, so you can drive your own styling — e.g. extra
// MapLibre layers reading the same feature-state, or a sidebar overlay.
const overlay = await TrafficIncidentOverlayModule.get(map, { focus: false });

Visibility and layer ordering

overlay.setVisible(false);
overlay.setVisible(true);
// Place the overlay above all other layers (default: below labels):
overlay.moveBeforeLayer('top');

Limitations

The Incident Details REST API returns a feature list, not a rendering-ready tile. Several tags that the vector-tile pipeline injects per-feature are absent from the REST response, so the overlay renders with reduced visual fidelity:

  • No road-class-aware width/offset (every incident is a uniform stripe centred on the road).
  • No declutter — every incident shows above the layer minzoom.
  • No secondary cause icon overlay (only the primary category icon renders).

If you need full visual fidelity, use TrafficIncidentsModule.

Overlapping incidents: when multiple incidents share the same point or stack along the same road segment, MapLibre’s symbol collision culling keeps only the highest-sort-key feature. Culled features are not reachable via queryRenderedFeatures. If you need to surface multiple incidents at one location, work directly with the FeatureCollection you passed to show().