Skip to content
Route with Elevation Profile

Draw a Route with an Elevation Profile in Maptoolkit Maps JS

A route on its own answers how far and how long. For anything walked or ridden, the climb decides whether the route is usable at all, and that means pairing the Routing API with an elevation profile. This example requests a hiking route, draws it, and charts its height along the way, with the chart and the map linked so hovering the profile shows the matching position on the route.

const API_KEY = 'YOUR_API_KEY';
    MTK.apiKey = API_KEY;

    const map = new maptoolkit.Map({
        container: 'map',
        apiKey: API_KEY,
        style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.hiking.json?api_key=${API_KEY}`,
        center: [11.389, 47.290],
        zoom: 12,
        attributionControl: { compact: false }
    });

    map.addControl(new maptoolkit.NavigationControl(), 'top-right');

    map.on('load', () => {
        const start = [11.3857, 47.2683];
        const end   = [11.3906, 47.3125];

        const url = new URL('https://routing.maptoolkit.net/route');
        url.searchParams.append('point', `${start[1]},${start[0]}`);
        url.searchParams.append('point', `${end[1]},${end[0]}`);
        url.searchParams.append('routeType', 'hike');
        url.searchParams.append('api_key', API_KEY);

        fetch(url)
            .then(r => r.json())
            .then(route => {
                const path = route.paths[0];
                const coordinates = polyline.decode(path.points).map(([lat, lng]) => [lng, lat]);

                map.addLayer({
                    id: 'route',
                    type: 'line',
                    source: { type: 'geojson', data: { type: 'Feature', geometry: { type: 'LineString', coordinates } } },
                    layout: { 'line-join': 'round', 'line-cap': 'round' },
                    paint: { 'line-color': '#2a3561', 'line-width': 5 }
                });

                map.addLayer({
                    id: 'highlight',
                    type: 'line',
                    source: { type: 'geojson', data: { type: 'Feature', geometry: { type: 'LineString', coordinates: [] } } },
                    layout: { 'line-join': 'round', 'line-cap': 'round' },
                    paint: { 'line-color': '#fa3f38', 'line-width': 5 }
                });

                map.fitBounds([[path.bbox[0], path.bbox[1]], [path.bbox[2], path.bbox[3]]], { padding: { top: 40, right: 40, bottom: 160, left: 40 } });

                const img = document.createElement('img');
                img.src = 'https://static.maptoolkit.net/sprites/toursprung/marker.svg';
                img.width = 29; img.height = 30;
                const marker = new maptoolkit.Marker({ element: img, anchor: 'bottom' }).setLngLat(coordinates[0]);

                const elevation = new MTK.ElevationProfile().addTo('profile').setLngLats(coordinates);

                elevation.on('mousemove', ev => {
                    !marker._map && marker.addTo(map);
                    marker.setLngLat(ev.lngLat);
                });
                elevation.on('mouseout', () => { marker._map && marker.remove(); });
                elevation.on('highlight', ev => {
                    map.getSource('highlight').setData({ type: 'Feature', geometry: { type: 'LineString', coordinates: ev.lngLats } });
                });
            });
    });
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Route with Elevation Profile - Maptoolkit Maps JS</title>
    <meta property="og:description" content="Draw a hiking route and chart its climb." />
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.js"></script>
    <link rel="stylesheet" href="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.css" />
    <link rel="stylesheet" href="https://static.maptoolkit.net/mtk/apis/v1.1/elevation.css" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/mapbox-polyline/1.2.1/polyline.min.js"></script>
    <style>
        html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
        #map { width: 100%; height: 100%; }
        #profile { position: absolute; bottom: 0; right: 0; width: 500px; max-width: calc(100% - 20px); margin: 0 10px 30px 0; z-index: 999; }
        #profile .mtk-elevation-profile { background: #fff; box-shadow: 0 0 15px #68686880; border-radius: 6px; }
        #profile .mtk-elevation-curve-stroke { stroke: #303f7e; stroke-width: 1rem; stroke-opacity: 1; }
        #profile .mtk-elevation-curve-fill { fill: #303f7e; }
        #profile .mtk-elevation-curve-bar { stroke: #fa3f38; }
        #profile .mtk-elevation-section { fill: rgba(250, 63, 56, 0.3); }
    </style>
</head>
<body>
<div id="map"></div>
<div id="profile"></div>
<script src="https://static.maptoolkit.net/mtk/apis/v1.1/elevation.js?api_key=YOUR_API_KEY"></script>
<script>
    const API_KEY = 'YOUR_API_KEY';
    MTK.apiKey = API_KEY;

    const map = new maptoolkit.Map({
        container: 'map',
        apiKey: API_KEY,
        style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.hiking.json?api_key=${API_KEY}`,
        center: [11.389, 47.290],
        zoom: 12,
        attributionControl: { compact: false }
    });

    map.addControl(new maptoolkit.NavigationControl(), 'top-right');

    map.on('load', () => {
        const start = [11.3857, 47.2683];
        const end   = [11.3906, 47.3125];

        const url = new URL('https://routing.maptoolkit.net/route');
        url.searchParams.append('point', `${start[1]},${start[0]}`);
        url.searchParams.append('point', `${end[1]},${end[0]}`);
        url.searchParams.append('routeType', 'hike');
        url.searchParams.append('api_key', API_KEY);

        fetch(url)
            .then(r => r.json())
            .then(route => {
                const path = route.paths[0];
                const coordinates = polyline.decode(path.points).map(([lat, lng]) => [lng, lat]);

                map.addLayer({
                    id: 'route',
                    type: 'line',
                    source: { type: 'geojson', data: { type: 'Feature', geometry: { type: 'LineString', coordinates } } },
                    layout: { 'line-join': 'round', 'line-cap': 'round' },
                    paint: { 'line-color': '#2a3561', 'line-width': 5 }
                });

                map.addLayer({
                    id: 'highlight',
                    type: 'line',
                    source: { type: 'geojson', data: { type: 'Feature', geometry: { type: 'LineString', coordinates: [] } } },
                    layout: { 'line-join': 'round', 'line-cap': 'round' },
                    paint: { 'line-color': '#fa3f38', 'line-width': 5 }
                });

                map.fitBounds([[path.bbox[0], path.bbox[1]], [path.bbox[2], path.bbox[3]]], { padding: { top: 40, right: 40, bottom: 160, left: 40 } });

                const img = document.createElement('img');
                img.src = 'https://static.maptoolkit.net/sprites/toursprung/marker.svg';
                img.width = 29; img.height = 30;
                const marker = new maptoolkit.Marker({ element: img, anchor: 'bottom' }).setLngLat(coordinates[0]);

                const elevation = new MTK.ElevationProfile().addTo('profile').setLngLats(coordinates);

                elevation.on('mousemove', ev => {
                    !marker._map && marker.addTo(map);
                    marker.setLngLat(ev.lngLat);
                });
                elevation.on('mouseout', () => { marker._map && marker.remove(); });
                elevation.on('highlight', ev => {
                    map.getSource('highlight').setData({ type: 'Feature', geometry: { type: 'LineString', coordinates: ev.lngLats } });
                });
            });
    });
</script>
</body>
</html>

Use the prompt below with any LLM to get the same result. Make sure the Maptoolkit MCP server is connected first — check out AI Integration & MCP to get started.

Use the Maptoolkit Connector. Create an interactive map with a hiking route from [11.3857, 47.2683] to [11.3906, 47.3125]. Draw the route and add an elevation profile below it that highlights the matching position on the map when hovered.

How it works

Three coordinate orders meet in this example and only one of them is [lng, lat]. The Routing API takes point as lat,lng, which is why start and end are written out reversed when they go into the query string. polyline.decode also returns [lat, lng] pairs, so the result is mapped into [lng, lat] before anything else touches it. From that line on, every coordinate in the file is GeoJSON order. Getting this wrong does not throw an error: the route lands in the Indian Ocean, which is where [47, 11] points.

MTK.apiKey is set separately from the map’s apiKey. The profile widget ships in elevation.js, a different bundle from Maps JS, and it resolves heights itself by posting the line to the Route Enhancement API at enhance.maptoolkit.net. The map’s key does not reach it, and the call does not appear in your code.

setLngLats(coordinates) takes the decoded route geometry directly. There is no elevation request to write and no need to thin the line first: the widget sends the whole geometry in a request body, which is why it is not subject to the query-string length limit that calculating the statistics yourself has to work around.

The three events tie the chart to the map. mousemove gives a coordinate for the marker, and highlight gives the coordinate array for the section under the cursor, which is fed into a second line layer drawn over the first. Without that second layer the chart is a picture beside a map instead of a control for it.

path.bbox is [minLng, minLat, maxLng, maxLat], already in GeoJSON order, so it goes into fitBounds as two corners. The bottom padding is larger than the rest to keep the route clear of the chart.

routeType: 'hike' is doing real work here. A car route through a valley produces a flat line and nothing to read, so the profile only earns its space on a route that climbs.

Next steps

The chart shows the shape of the climb. The numbers people compare before committing to a route, total ascent, descent and the steepest section, come out of the same elevation array without another request, and they belong next to the chart.

What the ground is made of matters as much as how steep it is on anything ridden. Colouring the line by surface shows where the asphalt ends, which is the other half of the question a cyclist is asking. The routeType values in the Routing API reference are what let the user ask for a bike rather than boots.