Skip to content

Map matching

This example snaps a series of raw points to the road network — useful for cleaning up noisy GPS traces from a tracking device. Click along a street to add points, then match them to the road network.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <script src="https://cdn.smartmaps.cloud/packages/smartmaps/smartmaps-gl/v2/umd/smartmaps-gl.min.js"></script>
        <style>
            body {
                margin: 0;
                padding: 0;
            }

            #map {
                height: 100vh;
            }

            #controls {
                position: absolute;
                top: 10px;
                left: 10px;
                z-index: 1;
                background: rgba(255, 255, 255, 0.9);
                padding: 10px;
                border-radius: 4px;
                font: 13px sans-serif;
                max-width: 240px;
            }

            #match-button,
            #reset-button {
                margin-top: 8px;
                margin-right: 6px;
                background-color: #18345c;
                color: white;
                border: none;
                padding: 8px 14px;
                cursor: pointer;
                border-radius: 4px;
            }

            #match-button:disabled {
                background-color: #9aa5b1;
                cursor: default;
            }

            .raw-point {
                width: 10px;
                height: 10px;
                border-radius: 50%;
                background: #9aa5b1;
                border: 2px solid #fff;
            }
        </style>
    </head>

    <body>
        <div id="controls">
            <strong>Click along a street to add GPS points</strong>
            <div id="status">0 points added.</div>
            <button id="match-button" disabled>Match to road</button>
            <button id="reset-button">Reset</button>
        </div>

        <div id="map"></div>

        <script>
            const map = new smartmapsgl.Map({
                apiKey: '[INSERT API-KEY]',
                container: 'map',
                center: { lat: 49.02164948779226, lng: 8.439330018049352 },
                zoom: 16,
                style: smartmapsgl.MapStyle.LIGHT
            });

            const statusEl = document.getElementById('status');
            const matchButton = document.getElementById('match-button');

            let points = [];
            let rawMarkers = [];

            function removeMatchLayer() {
                if (map.getLayer('matched-route')) map.removeLayer('matched-route');
                if (map.getSource('matched-route')) map.removeSource('matched-route');
            }

            function reset() {
                points = [];
                rawMarkers.forEach((marker) => marker.remove());
                rawMarkers = [];
                removeMatchLayer();
                matchButton.disabled = true;
                statusEl.textContent = '0 points added.';
            }

            document.getElementById('reset-button').addEventListener('click', reset);

            map.on('click', (event) => {
                points.push({ lng: event.lngLat.lng, lat: event.lngLat.lat });

                const el = document.createElement('div');
                el.className = 'raw-point';
                rawMarkers.push(new smartmapsgl.Marker({ element: el }).setLngLat(event.lngLat).addTo(map));

                statusEl.textContent = `${points.length} point${points.length === 1 ? '' : 's'} added.`;
                matchButton.disabled = points.length < 2;
            });

            matchButton.addEventListener('click', async () => {
                statusEl.textContent = 'Matching to road network…';

                try {
                    const response = await fetch(
                        'https://www.yellowmap.de/api_rst/v2/geojson/route?apiKey=[INSERT API-KEY]',
                        {
                            method: 'POST',
                            headers: { 'Content-Type': 'application/json' },
                            body: JSON.stringify({
                                type: 'FeatureCollection',
                                routingparams: {
                                    type: 'MATCH',
                                    isoLocale: 'en-GB',
                                    coordFormatOut: 'GEODECIMAL_POINT',
                                    speedProfile: 'FAST',
                                    routingTimeMode: 'ARRIVAL',
                                    channel: ''
                                },
                                authentication: { channel: '' },
                                crs: { type: 'name', properties: { name: 'urn:ogc:def:crs:OGC::CRS84' } },
                                features: [
                                    {
                                        type: 'Feature',
                                        geometry: {
                                            type: 'MultiPoint',
                                            coordinates: points.map((p) => [p.lng, p.lat])
                                        }
                                    }
                                ]
                            })
                        }
                    );

                    if (!response.ok) throw new Error(`Server error: ${response.status}`);

                    const result = await response.json();
                    const matched = result.features[0];
                    const destinations = matched.properties.matchDestinations;
                    const matchedCount = destinations.filter((d) => d.isMatched).length;

                    removeMatchLayer();
                    map.addSource('matched-route', { type: 'geojson', data: matched });
                    map.addLayer({
                        id: 'matched-route',
                        type: 'line',
                        source: 'matched-route',
                        layout: { 'line-join': 'round', 'line-cap': 'round' },
                        paint: { 'line-color': '#0A5FA0', 'line-width': 5 }
                    });

                    // distance/duration live on the FeatureCollection's top-level
                    // properties, NOT on the matched feature's properties
                    // (which only holds matchDestinations).
                    const km = (result.properties.distance / 1000).toFixed(2);
                    statusEl.textContent = `Matched ${matchedCount}/${destinations.length} points. Route: ${km} km.`;
                } catch (error) {
                    console.error('Error matching points:', error);
                    statusEl.textContent = 'Failed to match points. Please try again.';
                }
            });
        </script>
    </body>
</html>

Notes

  • Input is a single MultiPoint feature holding every raw point — not one Point feature per point.
  • The response has one feature: a LineString geometry for the matched route, plus properties.matchDestinations, one entry per input point in the same order they were sent, each with isMatched, distance (metres from the original point to its matched point), inPoint, and matchedPoint.
  • A point can fail to match (isMatched: false) if it's too far from any road — check this instead of assuming every point matched.

See the Map Matching reference for the full parameter list. Unlike Route, Trip, Isochrone and Matrix, Map Matching has no wrapper class in the @smartmaps/routing npm package — call the REST endpoint directly, as shown above.