Routes

Working with Routes

findBestWaypointInsertionIndex

Finds the optimal index to insert a new waypoint into an existing route. The algorithm projects each waypoint onto the route line to determine where the new waypoint fits most naturally along the route’s progression.

import { findBestWaypointInsertionIndex } from '@tomtom-org/maps-sdk/core';
const route = await calculateRoute({
key: 'your-api-key',
locations: [[4.9, 52.3], [5.0, 52.4]]
});
const waypoints = [
[4.9, 52.3], // origin
[5.0, 52.4] // destination
];
const newWaypoint = [4.95, 52.35];
const insertIndex = findBestWaypointInsertionIndex(
route.features[0],
waypoints,
newWaypoint
);
// Returns: 1 (insert between origin and destination)
const updatedWaypoints = [
...waypoints.slice(0, insertIndex),
newWaypoint,
...waypoints.slice(insertIndex)
];
// Result: [[4.9, 52.3], [4.95, 52.35], [5.0, 52.4]]

Parameters:

  • route – The existing calculated route Feature with LineString geometry
  • existingWaypoints – Array of existing waypoints in order (origin, intermediates, destination)
  • newWaypoint – The new waypoint to insert

Returns: number – The index where the new waypoint should be inserted. 0 means insert at the beginning; existingWaypoints.length means append at the end. Returns 0 if the route has no coordinates or there are fewer than 2 existing waypoints.

findBestWaypointInsertionIndices

Multi-waypoint variant of findBestWaypointInsertionIndex. Projects every existing and new waypoint onto the route exactly once, then returns one slot index per new waypoint. Use this instead of calling the singular variant in a loop — it scales as O(n + m) on projections rather than O(n · m).

import { findBestWaypointInsertionIndices } from '@tomtom-org/maps-sdk/core';
const route = await calculateRoute({
key: 'your-api-key',
locations: [[4.9, 52.3], [5.1, 52.5]]
});
const existingWaypoints = [
[4.9, 52.3], // origin
[5.1, 52.5] // destination
];
const newWaypoints = [
[5.05, 52.45], // closer to destination
[4.95, 52.35] // closer to origin
];
const slots = findBestWaypointInsertionIndices(
route.features[0],
existingWaypoints,
newWaypoints
);
// Returns: [1, 1] — both new waypoints fall between origin and destination

Multiple new waypoints may share the same slot. When you also need them merged into the existing array in correct along-route order, use withInsertedWaypoints — it adds within-slot ranking on top of these slot indices.

Parameters:

  • route – The existing calculated route Feature with LineString geometry
  • existingWaypoints – Array of existing waypoints in order (origin, intermediates, destination)
  • newWaypoints – The waypoints whose insertion slots to compute

Returns: number[] – An array of insertion slot indices, one per new waypoint, in input order. Returns an empty array for empty newWaypoints. When the route has no geometry or fewer than 2 existing waypoints, all slots are 0.

withInsertedWaypoint

Convenience function that combines finding the best insertion index and inserting the waypoint in one step. Returns a new array without modifying the original.

import type { Route, Waypoint, WaypointLike } from '@tomtom-org/maps-sdk/core';
import { bboxFromGeoJSON, TomTomConfig, withInsertedWaypoint } from '@tomtom-org/maps-sdk/core';
import type { WaypointDisplayProps } from '@tomtom-org/maps-sdk/map';
import { BaseMapModule, MIDDLE_INDEX, RoutingModule, TomTomMap } from '@tomtom-org/maps-sdk/map';
import { calculateRoute, geocodeOne } from '@tomtom-org/maps-sdk/services';
import { LngLat } from 'maplibre-gl';
import { API_KEY } from './config';
import { buildAddStopHTML, buildRemoveStopHTML, createStopPopup } from './popup';
import './style.css';
import { initTogglePanel } from './togglePanel';

// (Set your own API key when working in your own environment)
TomTomConfig.instance.put({ apiKey: API_KEY });

(async () => {
    const [origin, destination] = await Promise.all(['Washington', 'New York'].map(geocodeOne));

    const map = new TomTomMap({
        mapLibre: {
            container: 'sdk-map',
            bounds: bboxFromGeoJSON([origin, destination]),
            fitBoundsOptions: { padding: 80 },
        },
    });

    const [routingModule, baseModule] = await Promise.all([RoutingModule.get(map), BaseMapModule.get(map)]);

    // --- State ---
    let stops: [number, number][] = [];
    let currentRoute: Route;
    let activePopup: ReturnType<typeof createStopPopup> | null = null;
    let isUpdating = false;

    // --- Helpers ---

    const getLocations = (): WaypointLike[] => [origin, ...stops, destination];

    const closeActivePopup = () => {
        activePopup?.remove();
        activePopup = null;
    };

    const setLoading = (loading: boolean) => {
        isUpdating = loading;
        map.mapLibreMap.getCanvas().style.cursor = loading ? 'wait' : '';
    };

    const recalculate = async () => {
        const routeResult = await calculateRoute({ locations: getLocations() });
        currentRoute = routeResult.features[0];
        routingModule.showWaypoints(getLocations());
        routingModule.showRoutes(routeResult);
    };

    const addStop = async (pos: [number, number]) => {
        if (isUpdating) return;
        setLoading(true);
        try {
            const updated = withInsertedWaypoint(currentRoute, getLocations(), pos);
            stops = updated.slice(1, -1) as [number, number][];
            await recalculate();
        } finally {
            setLoading(false);
        }
    };

    const removeStop = async (index: number) => {
        if (isUpdating) return;
        setLoading(true);
        try {
            stops = stops.filter((_, i) => i !== index);
            await recalculate();
        } finally {
            setLoading(false);
        }
    };

    // --- Initial route ---
    const initialResult = await calculateRoute({ locations: [origin, destination] });
    currentRoute = initialResult.features[0];
    routingModule.showWaypoints([origin, destination]);
    routingModule.showRoutes(initialResult);

    // --- Waypoint click: show "Remove stop" popup for intermediate stops ---
    routingModule.events.user.waypoints.on('click', (waypoint: Waypoint<WaypointDisplayProps>, lngLat: LngLat) => {
        if (waypoint.properties.indexType !== MIDDLE_INDEX || isUpdating) return;

        closeActivePopup();

        // waypoint.properties.index is its position in [origin, ...stops, destination],
        // so the corresponding index in the stops array is index - 1.
        const stopIndex = waypoint.properties.index - 1;

        const popup = createStopPopup().setLngLat(lngLat).setHTML(buildRemoveStopHTML()).addTo(map.mapLibreMap);

        activePopup = popup;

        popup
            .getElement()
            .querySelector('.stop-popup-btn')
            ?.addEventListener('click', () => {
                closeActivePopup();
                removeStop(stopIndex);
            });
    });

    // --- Map click: show "Add Stop" popup (or dismiss existing popup) ---
    baseModule.events.on('click', (_: unknown, lngLat: LngLat) => {
        if (isUpdating) return;

        if (activePopup) {
            closeActivePopup();
            return;
        }

        const popup = createStopPopup().setLngLat(lngLat).setHTML(buildAddStopHTML()).addTo(map.mapLibreMap);

        activePopup = popup;

        popup
            .getElement()
            .querySelector('.stop-popup-btn')
            ?.addEventListener('click', () => {
                closeActivePopup();
                addStop(lngLat.toArray());
            });
    });

    initTogglePanel();
})();

import { withInsertedWaypoint } from '@tomtom-org/maps-sdk/core';
const route = await calculateRoute({
key: 'your-api-key',
locations: [[4.9, 52.3], [5.0, 52.4]]
});
const waypoints = [
[4.9, 52.3], // origin
[5.0, 52.4] // destination
];
const newWaypoint = [4.95, 52.35];
const updatedWaypoints = withInsertedWaypoint(route.features[0], waypoints, newWaypoint);
// Returns: [[4.9, 52.3], [4.95, 52.35], [5.0, 52.4]]
// Recalculate route with the updated waypoints
const updatedRoute = await calculateRoute({
key: 'your-api-key',
locations: updatedWaypoints
});

Parameters:

  • route – The existing calculated route Feature with LineString geometry
  • existingWaypoints – Array of existing waypoints in order (origin, intermediates, destination)
  • newWaypoint – The new waypoint to insert

Returns: WaypointLike[] – A new array with the waypoint inserted at the optimal position.

withInsertedWaypoints

Multi-waypoint variant of withInsertedWaypoint. Unlike a sequential one-at-a-time insertion, this function projects every existing and new waypoint onto the route exactly once, then merges them into the existing array in correct along-route order. The result is independent of input order.

import { bboxFromGeoJSON, TomTomConfig, type Waypoint, withInsertedWaypoints } from '@tomtom-org/maps-sdk/core';
import { PlacesModule, RoutingModule, TomTomMap } from '@tomtom-org/maps-sdk/map';
import { calculateRoute, geocodeOne, search } from '@tomtom-org/maps-sdk/services';
import { API_KEY } from './config';
import './style.css';

TomTomConfig.instance.put({ apiKey: API_KEY });

(async () => {
    const initialWaypoints: Waypoint[] = await Promise.all(['Paris', 'Amsterdam'].map(geocodeOne));

    const map = new TomTomMap({
        mapLibre: {
            container: 'sdk-map',
            bounds: bboxFromGeoJSON(initialWaypoints),
            fitBoundsOptions: { padding: 80 },
        },
    });

    const routingModule = await RoutingModule.get(map);
    routingModule.showWaypoints(initialWaypoints);
    const initialRoutes = await calculateRoute({ locations: initialWaypoints });

    routingModule.showRoutes(initialRoutes);
    const initialRoute = initialRoutes.features[0];

    const stopsAlongRoute = await search({
        poiCategories: ['ELECTRIC_VEHICLE_STATION'],
        route: initialRoute,
        minPowerKW: 150,
        maxDetourTimeSeconds: 60,
        limit: 10,
    });
    const placesModule = await PlacesModule.get(map);
    placesModule.show(stopsAlongRoute);

    // Insert all charging stops at their optimal along-route positions in one call.
    // `withInsertedWaypoints` projects every existing and new waypoint once against the
    // route, then sorts the new ones into along-route order — independent of input order.
    const updatedWaypoints = withInsertedWaypoints(initialRoute, initialWaypoints, stopsAlongRoute.features);
    const updatedRoutes = await calculateRoute({ locations: updatedWaypoints });

    placesModule.clear();
    routingModule.showWaypoints(updatedWaypoints);
    routingModule.showRoutes(updatedRoutes);
})();

The merge process:

  1. Compute each new waypoint’s insertion slot via findBestWaypointInsertionIndices.
  2. Within each slot, rank the new waypoints by their along-route location (with stable tie-breaking on input order).

This is the right tool whenever you have N new stops to merge — for example, results from alongRouteSearch. Don’t loop withInsertedWaypoint to insert N stops; the plural variant projects everything once (O(n + m) instead of O(n · m)) and produces a deterministic along-route order independent of input order.

import { withInsertedWaypoints } from '@tomtom-org/maps-sdk/core';
const route = await calculateRoute({
key: 'your-api-key',
locations: [[4.9, 52.3], [5.1, 52.5]]
});
const existingWaypoints = [
[4.9, 52.3], // origin
[5.1, 52.5] // destination
];
// New stops can be passed in any order — the result settles into along-route order
const newWaypoints = [
[5.05, 52.45], // closer to destination
[4.95, 52.35] // closer to origin
];
const updatedWaypoints = withInsertedWaypoints(
route.features[0],
existingWaypoints,
newWaypoints
);
// Returns: [[4.9, 52.3], [4.95, 52.35], [5.05, 52.45], [5.1, 52.5]]
const updatedRoute = await calculateRoute({
key: 'your-api-key',
locations: updatedWaypoints
});

Parameters:

  • route – The existing calculated route Feature with LineString geometry
  • existingWaypoints – Array of existing waypoints in order (origin, intermediates, destination)
  • newWaypoints – The waypoints to insert

Returns: WaypointLike[] – A new array with the new waypoints spliced in at their optimal positions and in along-route order. An empty newWaypoints returns a shallow copy of existingWaypoints. When the route has no geometry or fewer than 2 existing waypoints, all new waypoints are prepended in input order.

getSectionBBox

Calculates a bounding box for a specific section of a route. The function samples three key points along the section — start, midpoint, and end — for an efficient yet accurate result.

import { getSectionBBox } from '@tomtom-org/maps-sdk/core';
const route = await calculateRoute({
key: 'your-api-key',
locations: [[4.9, 52.3], [5.1, 52.5]]
});
const routeFeature = route.features[0];
const countrySections = routeFeature.properties.sections.countries;
for (const section of countrySections) {
const bbox = getSectionBBox(routeFeature, section);
if (bbox) {
// Fit the map to this section
map.fitBounds(bbox, { padding: 40 });
}
}

Parameters:

  • route – The route containing the section
  • section – The section to calculate the bbox for, with startPointIndex and endPointIndex referencing coordinates in the route geometry

Returns: [minLng, minLat, maxLng, maxLat] | undefined – The bounding box, or undefined if the route has no coordinates or the section indices are invalid.

calculateProgressAtRoutePoint

Interpolates the cumulative traveled distance and time at an arbitrary coordinate index along a route. Uses the route’s progress array to linearly interpolate between bracketing entries.

import { calculateProgressAtRoutePoint } from '@tomtom-org/maps-sdk/core';
const route = await calculateRoute({
key: 'your-api-key',
locations: [[4.9, 52.3], [5.1, 52.5]]
});
const routeFeature = route.features[0];
// Get the cumulative distance and time at coordinate index 10
const progress = calculateProgressAtRoutePoint(routeFeature, 10);
if (progress) {
console.log(`${progress.distanceInMeters} m from start`);
console.log(`${progress.travelTimeInSeconds} s from start`);
}

Parameters:

  • route – The route whose properties.progress will be used for interpolation
  • pathIndex – Zero-based index into the route’s coordinate array

Returns: RouteProgressAtPoint | undefined – Interpolated { distanceInMeters, travelTimeInSeconds }, or undefined if the index is out of range or progress data is missing.

getRouteProgressBetween

Calculates distance and travel time for a segment between two arbitrary coordinate indices on a route.

import { getRouteProgressBetween } from '@tomtom-org/maps-sdk/core';
const routeFeature = route.features[0];
const segmentProgress = getRouteProgressBetween(routeFeature, 5, 20);
if (segmentProgress) {
console.log(`Segment starts ${segmentProgress.start.distanceInMeters} m from route origin`);
console.log(`Segment ends ${segmentProgress.end.distanceInMeters} m from route origin`);
console.log(`Segment length ${segmentProgress.delta.distanceInMeters} m`);
console.log(`Segment takes ${segmentProgress.delta.travelTimeInSeconds} s`);
}

Parameters:

  • route – The route whose properties.progress will be used for interpolation
  • startPathIndex – Zero-based index of the segment start in the route’s coordinate array
  • endPathIndex – Zero-based index of the segment end in the route’s coordinate array

Returns: RouteSegmentProgress | undefined – Object with start, end, and delta progress values, or undefined if either index cannot be interpolated.

getRouteProgressForSection

Convenience wrapper around getRouteProgressBetween that accepts a section object directly, using its startPointIndex and endPointIndex as the segment bounds.

import { getRouteProgressForSection } from '@tomtom-org/maps-sdk/core';
const routeFeature = route.features[0];
const trafficSections = routeFeature.properties.sections.traffic ?? [];
for (const section of trafficSections) {
const progress = getRouteProgressForSection(routeFeature, section);
if (progress) {
console.log(`Traffic section starts at ${progress.start.distanceInMeters} m`);
console.log(`Traffic section takes ${progress.delta.travelTimeInSeconds} s extra`);
}
}

Parameters:

  • route – The route whose properties.progress will be used for interpolation
  • section – A section object with startPointIndex and endPointIndex

Returns: RouteSegmentProgress | undefined – Same as getRouteProgressBetween.

getCoordinateAtRouteProgress

Returns the geographic position and cumulative progress values at the point along a route identified by elapsed time, traveled distance, or an absolute clock time. Accepts a RouteProgressQuery discriminated union — provide exactly one of the three variants.

import { getCoordinateAtRouteProgress } from '@tomtom-org/maps-sdk/core';
const routeFeature = route.features[0];
// By elapsed travel time
const byTime = getCoordinateAtRouteProgress(routeFeature, {
traveledTimeInSeconds: 600
});
// By traveled distance
const byDistance = getCoordinateAtRouteProgress(routeFeature, {
traveledDistanceInMeters: 5000
});
// By absolute clock time — elapsed seconds are derived from the route departure time
const byClockTime = getCoordinateAtRouteProgress(routeFeature, {
clockTime: new Date('2025-06-01T09:10:00Z')
});
if (byTime) {
const [lng, lat] = byTime.position;
console.log(`Position after 10 min: [${lng}, ${lat}]`);
console.log(`Distance covered: ${byTime.distanceInMeters} m`);
}

Values beyond the route’s total range are clamped to the start or end point.

Parameters:

  • route – The route to query. Must contain properties.progress data
  • query – A RouteProgressQuery — exactly one of:
    • { traveledTimeInSeconds: number } – elapsed seconds from route start
    • { traveledDistanceInMeters: number } – meters traveled from route start
    • { clockTime: Date } – absolute point in time; elapsed seconds are derived using route.properties.summary.departureTime

Returns: RouteCoordinateAtProgress | undefined – Object with position ([longitude, latitude]), travelTimeInSeconds, and distanceInMeters, or undefined if the route has no progress data, the clock time precedes the departure time, or the progress data is incomplete.

getProgressAtNearestRoutePoint

Snaps an arbitrary geographic point to the nearest position on a route and returns the interpolated coordinate and cumulative progress values at that snapped position. Useful for showing progress info when the user hovers or clicks near a route line.

import { getProgressAtNearestRoutePoint } from '@tomtom-org/maps-sdk/core';
const routeFeature = routeResult.features[0];
// Pass any HasLngLat value — [lng, lat] tuple, { lng, lat } object, a GeoJSON Point, etc.
const result = getProgressAtNearestRoutePoint(routeFeature, { lng: 4.92, lat: 52.32 });
if (result) {
const [lng, lat] = result.position;
console.log(`Snapped to: [${lng}, ${lat}]`);
console.log(`${result.distanceInMeters} m from route start`);
console.log(`${result.travelTimeInSeconds} s travel time from route start`);
}

Parameters:

  • route – The route to query. Must contain properties.progress data
  • point – The reference location to snap to the route. Accepts any HasLngLat value: a [longitude, latitude] tuple, a { lng, lat } object, a GeoJSON Point feature, etc.

Returns: RouteCoordinateAtProgress | undefined – Object with position ([longitude, latitude]), travelTimeInSeconds, and distanceInMeters at the nearest point on the route, or undefined if the route has no progress data, fewer than 2 coordinates, or the progress data is incomplete for the snapped position.