Skip to main content

Route path report as map source and layer

Display a route path report as a map source and layer.

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <link rel="stylesheet" href="https://maps-sdk.trimblemaps.com/v4/trimblemaps-4.2.7.css" />
        <script src="https://maps-sdk.trimblemaps.com/v4/trimblemaps-4.2.7.js"></script>
        <style>
            body { margin: 0; padding: 0; }
            html, body, #map { height: 100%; }

            .controls {
            position: absolute;
            top: 10px;
            left: 10px;
            z-index: 1;
            background: rgba(255, 255, 255, 0.95);
            padding: 10px 14px;
            border-radius: 6px;
            box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
        }
        .controls button {
            font: 12px/20px system-ui, sans-serif;
            background: #3386c0;
            color: #fff;
            padding: 8px 16px;
            border: none;
            cursor: pointer;
            border-radius: 3px;
            margin-right: 4px;
        }
        .controls button:hover { background: #4ea0da; }
        .controls label {
            margin-left: 12px;
            font: 12px/20px system-ui, sans-serif;
            cursor: pointer;
            user-select: none;
            color: #333;
        }
        .controls input[type="checkbox"] { margin-right: 4px; cursor: pointer; }
        #status {
            margin-left: 12px;
            color: #333;
            font: 12px/20px system-ui, sans-serif;
        }
        </style>
    </head>
    <body>
        <div id="map"></div>

        <div class="controls">
        <button id="fetchRoute">Fetch route path & add to map</button>
        <label><input type="checkbox" id="chkRoutePath" checked> Route path</label>
        <label><input type="checkbox" id="chkStops" checked> Stops</label>
        <span id="status"></span>
        </div>

        <script>
            TrimbleMaps.setAPIKey('YOUR_API_KEY_HERE');
            const map = new TrimbleMaps.Map({
              container: 'map', // container id
              style: TrimbleMaps.Common.Style.TRANSPORTATION, // hosted style id
              center: [-118.31, 33.72],
              zoom: 12
        });

        const ROUTE_SOURCE_ID = 'derived-route-path';
        const ROUTE_LAYER_ID = 'derived-route-line';
        const STOPS_SOURCE_ID = 'derived-route-stops';
        const STOPS_LAYER_ID = 'derived-route-stops-layer';
        let stopMarkers = [];

        function setStatus(msg, isError) {
            const el = document.getElementById('status');
            if (el) {
                el.textContent = msg;
                el.style.color = isError ? '#c00' : '#333';
            }
        }

        /**
         * Normalize the Route path API response to a GeoJSON Feature.
         * The derived route path API returns the response body as a string (JSON);
         * it must be parsed first. The API returns GeoJSON Feature with
         * geometry.type 'MultiLineString' or 'LineString'.
         * @param {Object} response - callback (error, response) where response = { data }
         * @returns {GeoJSON.Feature | null}
         */
        function routePathResponseToGeoJSON(response) {
            if (!response || response.data === undefined) return null;
            let data = response.data;
            // POST response is raw string – must parse (same as route.ts)
            if (typeof data === 'string') {
                try {
                    data = JSON.parse(data);
                } catch (e) {
                    console.warn('Route path response is not valid JSON', e);
                    return null;
                }
            }

            // Already a GeoJSON Feature (how derivedRoute/routePath returns it)
            if (data && data.type === 'Feature' && data.geometry && data.geometry.coordinates) {
                return {
                    type: 'Feature',
                    properties: data.properties || {},
                    geometry: data.geometry
                };
            }

            // Alternate shapes: routePath array or plain coordinates array
            let coordinates = null;
            if (Array.isArray(data)) {
                coordinates = data;
            } else if (data && data.routePath && Array.isArray(data.routePath)) {
                coordinates = data.routePath;
            } else if (data && data.geometry && data.geometry.coordinates) {
                const geom = data.geometry;
                if (geom.type === 'LineString' || geom.type === 'MultiLineString') {
                    return {
                        type: 'Feature',
                        properties: {},
                        geometry: {type: geom.type, coordinates: geom.coordinates}
                    };
                }
            }

            if (!coordinates || coordinates.length < 2) return null;
            const normalized = coordinates.map((c) => {
                if (Array.isArray(c)) return [Number(c[0]), Number(c[1])];
                if (c && (c.lng !== undefined || c.lon !== undefined)) return [Number(c.lng ?? c.lon), Number(c.lat)];
                return c;
            });
            return {
                type: 'Feature',
                properties: {},
                geometry: {type: 'LineString', coordinates: normalized}
            };
        }

        /**
         * Add or update the route path as a GeoJSON source and line layer on the map.
         * Call only after map has loaded. GeoJSON line layers support both
         * LineString and MultiLineString.
         * @param {Object} geojson - GeoJSON Feature with LineString or MultiLineString
         */
        function addRoutePathToMap(geojson) {
            if (!geojson || !geojson.geometry || !geojson.geometry.coordinates) return;
            if (!map.isStyleLoaded()) {
                console.warn('Map style not ready; add route path after map load.');
                return;
            }

            const mapSource = map.getSource(ROUTE_SOURCE_ID);
            if (mapSource) {
                mapSource.setData(geojson);
            } else {
                map.addSource(ROUTE_SOURCE_ID, {
                    type: 'geojson',
                    data: geojson
                });
                map.addLayer({
                    id: ROUTE_LAYER_ID,
                    type: 'line',
                    source: ROUTE_SOURCE_ID,
                    layout: {
                        'line-join': 'round',
                        'line-cap': 'round'
                    },
                    paint: {
                        'line-color': '#3386c0',
                        'line-width': 6,
                        'line-opacity': 0.9
                    }
                });
            }

            try {
                const bounds = new TrimbleMaps.LngLatBounds();
                const coords = geojson.geometry.type === 'LineString' ?
            geojson.geometry.coordinates :
            geojson.geometry.coordinates.flat(1);
                coords.forEach((c) => { bounds.extend(c); });
                map.fitBounds(bounds, {padding: 40, maxZoom: 14});
            } catch (e) { /* ignore */ }
        }

        /**
         * Get start and end coordinates from the route path GeoJSON (first and last point of the route).
         * @param {Object} routeGeojson - GeoJSON Feature with LineString or MultiLineString
         * @returns {Array<[number,number]>} [startCoord, endCoord] or empty if invalid
         */
        function getStopsFromRoutePath(routeGeojson) {
            if (!routeGeojson || !routeGeojson.geometry || !routeGeojson.geometry.coordinates) return [];
            const coords = routeGeojson.geometry.coordinates;
            let first, last;
            if (routeGeojson.geometry.type === 'LineString') {
                if (coords.length < 2) return [];
                first = coords[0];
                last = coords[coords.length - 1];
            } else if (routeGeojson.geometry.type === 'MultiLineString') {
                if (coords.length === 0) return [];
                first = coords[0][0];
                last = coords[coords.length - 1][coords[coords.length - 1].length - 1];
            } else {
                return [];
            }
            return [first, last];
        }

        function getStopCoords(stop) {
            if (Array.isArray(stop)) return [Number(stop[0]), Number(stop[1])];
            if (typeof stop.toArray === 'function') return stop.toArray();
            return [Number(stop.lng ?? stop.lon), Number(stop.lat)];
        }

        function clearStopMarkers() {
            stopMarkers.forEach((m) => { m.remove(); });
            stopMarkers = [];
        }

        function addStopMarkers(stops, options) {
            clearStopMarkers();
            if (!stops || !Array.isArray(stops) || stops.length === 0) return;
            const labels = (options && options.labels) ? options.labels : ['Start', 'End'];
            stops.forEach((stop, i) => {
                const coords = getStopCoords(stop);
                const marker = new TrimbleMaps.Marker({color: '#3386c0'})
                    .setLngLat(coords)
                    .setPopup(new TrimbleMaps.Popup({offset: 25}).setText(labels[i] || `Stop ${i + 1}`))
                    .addTo(map);
                stopMarkers.push(marker);
            });
            updateStopMarkersVisibility();
        }

        function updateStopMarkersVisibility() {
            const show = document.getElementById('chkStops').checked;
            stopMarkers.forEach((m) => {
                const el = m.getElement();
                if (el) el.style.display = show ? '' : 'none';
            });
        }

        /**
         * Add or update stops as a GeoJSON source and circle layer on the map.
         * Also adds DOM Markers so stops are always visible regardless of style layers.
         */
        function addStopsToMap(stops, options) {
            if (!stops || !Array.isArray(stops) || stops.length === 0) return;

            const features = stops.map((stop, i) => {
                const coords = getStopCoords(stop);
                const label = (options && options.labels && options.labels[i]) || (i === 0 ? 'Start' : (i === stops.length - 1 ? 'End' : `Stop ${i + 1}`));
                return {
                    type: 'Feature',
                    properties: {stopType: i === 0 ? 'start' : (i === stops.length - 1 ? 'end' : 'stop'), label},
                    geometry: {type: 'Point', coordinates: coords}
                };
            });
            const geojson = {type: 'FeatureCollection', features};

            addStopMarkers(stops, options);

            if (map.isStyleLoaded()) {
                const mapSource = map.getSource(STOPS_SOURCE_ID);
                if (mapSource) {
                    mapSource.setData(geojson);
                } else {
                    map.addSource(STOPS_SOURCE_ID, {type: 'geojson', data: geojson});
                    const showStops = document.getElementById('chkStops').checked;
                    map.addLayer({
                        id: STOPS_LAYER_ID,
                        type: 'circle',
                        source: STOPS_SOURCE_ID,
                        minzoom: 0,
                        layout: {'visibility': showStops ? 'visible' : 'none'},
                        paint: {
                            'circle-radius': 10,
                            'circle-color': '#3386c0',
                            'circle-stroke-width': 2,
                            'circle-stroke-color': '#fff'
                        }
                    });
                    moveRouteAndStopsLayersToTop();
                }
            } else {
                map.once('idle', () => {
                    if (map.getSource(STOPS_SOURCE_ID)) {
                        map.getSource(STOPS_SOURCE_ID).setData(geojson);
                    } else {
                        map.addSource(STOPS_SOURCE_ID, {type: 'geojson', data: geojson});
                        const showStops = document.getElementById('chkStops').checked;
                        map.addLayer({
                            id: STOPS_LAYER_ID,
                            type: 'circle',
                            source: STOPS_SOURCE_ID,
                            minzoom: 0,
                            layout: {'visibility': showStops ? 'visible' : 'none'},
                            paint: {
                                'circle-radius': 10,
                                'circle-color': '#3386c0',
                                'circle-stroke-width': 2,
                                'circle-stroke-color': '#fff'
                            }
                        });
                        moveRouteAndStopsLayersToTop();
                    }
                });
            }
            updateLayerVisibility();
        }

        function moveRouteAndStopsLayersToTop() {  // To keep the route path and stop circles on top of the base map.
            try {
                const layers = map.getStyle().layers;
                if (!layers || !layers.length) return;
                let topId = layers[layers.length - 1].id;
                if (map.getLayer(ROUTE_LAYER_ID) && topId !== ROUTE_LAYER_ID) {
                    map.moveLayer(topId, ROUTE_LAYER_ID);
                }
                if (map.getLayer(STOPS_LAYER_ID)) {
                    topId = map.getStyle().layers[map.getStyle().layers.length - 1].id;
                    if (topId !== STOPS_LAYER_ID) map.moveLayer(topId, STOPS_LAYER_ID);
                }
            } catch (e) { /* ignore */ }
        }

        function updateLayerVisibility() {
            const showRoute = document.getElementById('chkRoutePath').checked;
            const showStops = document.getElementById('chkStops').checked;
            if (map.getLayer(ROUTE_LAYER_ID)) {
                map.setLayoutProperty(ROUTE_LAYER_ID, 'visibility', showRoute ? 'visible' : 'none');
            }
            if (map.getLayer(STOPS_LAYER_ID)) {
                map.setLayoutProperty(STOPS_LAYER_ID, 'visibility', showStops ? 'visible' : 'none');
            }
            updateStopMarkersVisibility();
        }

        function fetchAndAddRoute() {
            setStatus('Loading route path...');
            const options = {
                routePings: [
                    new TrimbleMaps.LngLat(-118.309434, 33.714154),
                    new TrimbleMaps.LngLat(-118.316631, 33.721061)
                ],
                offRouteMiles: 0,
                highwayOnly: false,
                reportType: TrimbleMaps.Common.ReportType.MILEAGE,
                region: TrimbleMaps.Common.Region.NA,
                routingOptions: {
                    vehicleType: TrimbleMaps.Common.VehicleType.TRUCK,
                    routeType: TrimbleMaps.Common.RouteType.PRACTICAL,
                    routeOptimization: TrimbleMaps.Common.RouteOptimization.NONE,
                    tollDiscourage: false,
                    bordersOpen: true,
                    overrideRestrict: false,
                    highwayOnly: false,
                    hazMatTypes: [TrimbleMaps.Common.HazMatType.NONE],
                    distanceUnits: TrimbleMaps.Common.DistanceUnit.MILES,
                    trkUnits: TrimbleMaps.Common.TruckUnit.ENGLISH,
                    trkHeight: '13\u00276"',
                    trkLength: '48\u00270"',
                    trkWidth: '96"',
                    trkWeight: '80000',
                    trkAxles: 5,
                    useSites: false,
                    overrideClass: TrimbleMaps.Common.ClassOverride.NONE
                },
                extendedOptions: {
                    estimatedTimeOptions: {
                        eTAETDType: TrimbleMaps.Common.ETAETDType.DEPART,
                        dateOption: TrimbleMaps.Common.DateOption.SPECIFIC,
                        dateAndTime: {
                            calendarDate: '4/23/2014',
                            dayOfWeek: TrimbleMaps.Common.DayOfWeek.SUNDAY,
                            timeOfDay: '6:00 AM',
                            timeZone: TrimbleMaps.Common.TimeZone.LOCAL
                        }
                    },
                    useTraffic: true,
                    truckStyle: TrimbleMaps.Common.TruckConfig.NONE,
                    includeVehicleRestrictedCleanupPoints: false
                },
                callback (error, response) {
            if (error) {
                console.error('Route path request failed', error);
                setStatus(`Error: ${error.message || error}`, true);
                return;
            }
            const geojson = routePathResponseToGeoJSON(response);
            if (geojson) {
                addRoutePathToMap(geojson);
                const stopsFromRoute = getStopsFromRoutePath(geojson);
                addStopsToMap(stopsFromRoute.length ? stopsFromRoute : options.routePings, {labels: ['Start', 'End']});
                updateLayerVisibility();
                setTimeout(moveRouteAndStopsLayersToTop, 100);
                setStatus('Route path and stops added to map.');
            } else {
                console.warn('Could not parse route path. Response:', response);
                setStatus('No route path in response. Check console.', true);
            }
        }
            };
            TrimbleMaps.Routing.postDerivedRoutePath(options);
        }

        map.on('load', () => {
            document.getElementById('fetchRoute').addEventListener('click', fetchAndAddRoute);
            document.getElementById('chkRoutePath').addEventListener('change', updateLayerVisibility);
            document.getElementById('chkStops').addEventListener('change', updateLayerVisibility);
        });
        </script>
    </body>
</html>
Last updated March 26, 2026.