Recommended way to get a map with a driving route on it?

Mike 20 Reputation points
2025-07-24T22:06:11.6933333+00:00

In Bing (Imagery/Map/Route/Routes), you could ask it for Road imagery that displays a route. What is the recommended way to do the same using Azure?

I'm currently using Azure Static maps (map/static?api-version=2024-04-01) with a path set to locations gotten from a Route Directions post, but quite often there are too many path locations, or the resulting static maps URL becomes too long.

Azure Maps
Azure Maps
An Azure service that provides geospatial APIs to add maps, spatial analytics, and mobility solutions to apps.
0 comments No comments
{count} votes

Accepted answer
  1. rbrundritt 21,191 Reputation points Microsoft Employee Moderator
    2025-07-25T17:00:19.0966667+00:00

    The Azure Maps Static image service doesn't currently support calculating and rendering routes. It can render lines and other shapes that you provide it. If you want to render a route line on a static image, you first need to calculate the route path, then pass that into the static image service. One tricky bit though is fitting all this data into a single URL. The handle this you would need to reduce the resolution of the line, most likely to match the resolution of the zoom level. Additionally, if you only want to show a part of a route, you could clip the line accordingly. I previously put the following code sample together for someone else who asked this question (can't seem to find that thread). You can try it out here: https://rbrundritt.azurewebsites.net/Demos/AzureMaps/StaticRouteImages/index.html

    Here is the code:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <title></title>
    
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    
        <script>
            var subscriptionKey = "<Your Azure Maps Key>";
            var startPosition = [-122.33028, 47.60323]; //Seattle
            var endPosition = [-122.124, 47.67491]; //Redmond
    
            var zoom = 11;
            var center = [-122.2372, 47.63];
    
            var tilesetId = "microsoft.base.road";
            var lineColor = "FF1493";
            var lineWidth = 5;
            var mapWidth = 600;
            var mapHeight = 400;
    
            var tileSize = 256;
    
            //Azure Maps Route API.
            var routeUrl = `https://atlas.microsoft.com/route/directions/json?api-version=1.0&query={query}&routeRepresentation=polyline&travelMode=car&view=Auto&subscription-key=${subscriptionKey}`;
    
            var staticMapUrl = `https://atlas.microsoft.com/map/static?api-version=2024-04-01&tilesetId=${tilesetId}&zoom=${zoom}&center=${center}&width=${mapWidth}&height=${mapHeight}`;
    
            function getMap() {
                var bounds = getImageBounds(center, zoom, mapWidth, mapHeight, tileSize);
                console.log(bounds)
    
                //Create the route request with the query being the start and end point in the format 'startLongitude,startLatitude:endLongitude,endLatitude'.
                var routeRequestURL = routeUrl.replace('{query}', `${startPosition[1]},${startPosition[0]}:${endPosition[1]},${endPosition[0]}`);
    
                // Process the request and render the route result on the map.
                fetch(routeRequestURL).then(r => r.json()).then(directions => {
                    //Extract the first route from the directions.
                    const route = directions.routes[0];
    
                    //Combine all leg positions into a single array.
                    routePositions = route.legs.flatMap(leg => leg.points.map(point => [point.longitude, point.latitude]));
    
                    var clippedPaths = clipPathEndsToBounds(routePositions, bounds);
    
                    let lowResClippedPaths = clippedPaths;
                    let zoomRes = zoom;
    
                    while (countPositions(lowResClippedPaths) > 100) {
                        lowResClippedPaths = [];
    
                        clippedPaths.forEach(path => {
                            var lowResPath = vertexReduction(path, zoomRes, tileSize);
    
                            if (lowResPath.length > 2) {
                                lowResClippedPaths.push(lowResPath);
                            }
                        });
                        zoomRes--;
                    } 
    
                    console.log(`${countPositions(lowResClippedPaths)} positions.`);
    
                    console.log(`Route rendered at zoom ${zoomRes} resolution.`);
    
                    var mapRequest = staticMapUrl;
    
                    if (lowResClippedPaths.length > 0) {
                        lowResClippedPaths.forEach(path => {
                            var positions = path.map(p => {
                                return `${roundSigDigits(p[0])} ${roundSigDigits(p[1])}`
                            }).join('|');
    
                            mapRequest += `&path=lc${lineColor}|lw${lineWidth}||${positions}`;
                        });
                    }
    
                    //Fetching as blob so I can pass subscription key in header.
                    fetch(mapRequest, {
                        headers: {
                            'subscription-key': subscriptionKey
                        }
                    }).then(r => r.blob()).then(mapImageBlob => {
                        let reader = new FileReader();
                        reader.onload = function () {
                            document.body.innerHTML += `<img src='${this.result}'/>`;
                        };
                        reader.readAsDataURL(mapImageBlob);
                    });
                });
            }
    
            function clipPathEndsToBounds(positions, bounds) {
                //Only get paths that cross the bounding box of the image.
                var clippedPaths = [];
    
                var path = [];
    
                for (let i = 1; i < positions.length; i++) {
                    if (doesLineCrossBBox(bounds, positions[i - 1], positions[i])) {
                        if (path.length === 0) {
                            path.push(positions[i - 1]); //Start the path offscreen
                        }
    
                        path.push(positions[i]);
                    }
                    else if (path.length > 1) {
                        path.push(positions[i]); //End the path off screen.
                        clippedPaths.push(path);
                        path = [];
                    }
                }
    
                if (path.length > 1) {
                    clippedPaths.push(path);
                }
    
                return clippedPaths;
            }
    
            function countPositions(clippedPaths) {
                return clippedPaths.map(p => p.length).reduce((accumulator, current) => accumulator + current, 0);
            }
    
            function boundsContains(bounds, position) {
                return bounds[0] <= position[0] &&
                    bounds[2] >= position[0] &&
                    bounds[1] <= position[1] &&
                    bounds[3] >= position[1];
            }
    
            function getImageBounds(center, zoom, mapWidth, mapHeight, tileSize) {
                //Get mercator pixel for center at specified zoom level and tile size.
                var centerPixel = mercatorPositionsToPixels([center], zoom, tileSize)[0];
    
                //Calculate top left and bottom right corner pixel, then convert to positions to create bounding box.
                var topLeftPixel = [
                    centerPixel[0] - mapWidth * 0.5,
                    centerPixel[1] - mapHeight * 0.5,
                ];
    
                var bottomRightPixel = [
                    centerPixel[0] + mapWidth * 0.5,
                    centerPixel[1] + mapHeight * 0.5,
                ];
    
                var corners = mercatorPixelsToPositions([topLeftPixel, bottomRightPixel], zoom, tileSize);
    
                return [
                    corners[0][0], //West
                    corners[1][1], //South
                    corners[1][0], //East
                    corners[0][1]  //North   
                ];
            }        
    
            //https://rbrundritt.wordpress.com/2011/12/03/vertex-reductionone-of-my-secret-weapons/
            //Tolerance = 360/256 * zoom ^ 4
            function vertexReduction(positions, zoom, tileSize) {
                var tolerance = 360 / (tileSize * Math.pow(zoom, 4));
    
                //Verify that there are at least 3 or more positions.
                if (positions === null || positions.length < 3) {
                    return positions;
                }
    
                const newPositions = [];
    
                //Store the initial location
                newPositions.push(positions[0]);
    
                let basePosition = positions[0];
    
                //Remove expensive square root calculation from loop and compare to squared tolerance
                const sqTolerance = tolerance * tolerance;
    
                for (let i = 1; i < positions.length - 1; i++)
                {
                    //Check to see if the distance between the base position and the position in question is outside the tolerance distance.
                    if (Math.pow(positions[i][1] - basePosition[1], 2) + Math.pow(positions[i][0] - basePosition[0], 2) > sqTolerance) {
                        //store the position and make it the new base position for comparison.
                        newPositions.push(positions[i]);
                        basePosition = positions[i];
                    }
                }
    
                //Store the last position
                newPositions.push(positions[positions.length - 1]);
    
                //Return the new set of positions
                return newPositions;
            }
    
            function mercatorPositionsToPixels(positions, zoom, tileSize) {
                const mapSize = tileSize * Math.pow(2, zoom);
    
                const pixels = [];
                let x;
                let y;
                let sinLatitude;
    
                for (let i = 0, len = positions.length; i < len; i++) {
                    sinLatitude = Math.sin(positions[i][1] * Math.PI / 180);
    
                    x = (positions[i][0] + 180) / 360;
                    y = 0.5 - Math.log((1 + sinLatitude) / (1 - sinLatitude)) / (4 * Math.PI);
    
                    pixels.push([
                        Math.round(x * mapSize),
                        Math.round(y * mapSize)
                    ]);
                }
                return pixels;
            }
    
            function mercatorPixelsToPositions(pixels, zoom, tileSize) {
                const mapSize = tileSize * Math.pow(2, zoom);
    
                const positions = [];
                let x;
                let y;
    
                for (let i = 0, len = pixels.length; i < len; i++) {
                    x = (pixels[i][0] / mapSize) - 0.5;
                    y = 0.5 - (pixels[i][1] / mapSize);
    
                    positions.push([
                        360 * x,
                        90 - 360 * Math.atan(Math.exp(-y * 2 * Math.PI)) / Math.PI
                    ]);
                }
    
                return positions;
            }
    
            //Rounds to 6 decimal places.
            function roundSigDigits(number) {
                return Math.round(number * 1000000) / 1000000;
            }
    
            function clipNumber(number, min, max) {
                return Math.max(Math.min(number, max), min);
            }
    
            /**
            * Check if a point is inside a bounding box.
            * @param {Array<number>} bbox - [minX, minY, maxX, maxY] bounding box coordinates.
            * @param {Array<number>} point - [x, y] coordinates of the point.
            * @returns {boolean} - True if the point is inside the bounding box, false otherwise.
            */
            function isPointInBBox(bbox, point) {
                const [minX, minY, maxX, maxY] = bbox;
                const [x, y] = point;
                return x >= minX && x <= maxX && y >= minY && y <= maxY;
            }
    
            /**
             * Check if two line segments (p1, p2) and (q1, q2) intersect.
             * @param {Array<number>} p1 - [x, y] coordinates of the first point of the first line segment.
             * @param {Array<number>} p2 - [x, y] coordinates of the second point of the first line segment.
             * @param {Array<number>} q1 - [x, y] coordinates of the first point of the second line segment.
             * @param {Array<number>} q2 - [x, y] coordinates of the second point of the second line segment.
             * @returns {boolean} - True if the lines intersect, false otherwise.
             */
            function doLinesIntersect(p1, p2, q1, q2) {
                // Helper function to calculate orientation
                function orientation(a, b, c) {
                    const val = (b[1] - a[1]) * (c[0] - b[0]) - (b[0] - a[0]) * (c[1] - b[1]);
                    if (val === 0) return 0; // collinear
                    return (val > 0) ? 1 : 2; // clock or counterclock wise
                }
    
                // Check if point q lies on segment pr
                function onSegment(p, q, r) {
                    return q[0] <= Math.max(p[0], r[0]) && q[0] >= Math.min(p[0], r[0]) &&
                        q[1] <= Math.max(p[1], r[1]) && q[1] >= Math.min(p[1], r[1]);
                }
    
                const o1 = orientation(p1, p2, q1);
                const o2 = orientation(p1, p2, q2);
                const o3 = orientation(q1, q2, p1);
                const o4 = orientation(q1, q2, p2);
    
                if (o1 !== o2 && o3 !== o4) return true;
    
                // Check for special cases of collinearity
                if (o1 === 0 && onSegment(p1, q1, p2)) return true;
                if (o2 === 0 && onSegment(p1, q2, p2)) return true;
                if (o3 === 0 && onSegment(q1, p1, q2)) return true;
                if (o4 === 0 && onSegment(q1, p2, q2)) return true;
    
                return false;
            }
    
            /**
             * Check if a line crosses a GeoJSON bounding box.
             * @param {Array<number>} bbox - [minX, minY, maxX, maxY] bounding box coordinates.
             * @param {Array<number>} start - [x, y] coordinates of the start point of the line.
             * @param {Array<number>} end - [x, y] coordinates of the end point of the line.
             * @returns {boolean} - True if the line crosses the bounding box, false otherwise.
             */
            function doesLineCrossBBox(bbox, start, end) {
                // Check if either endpoint is inside the bounding box
                if (isPointInBBox(bbox, start) || isPointInBBox(bbox, end)) {
                    return true;
                }
    
                // Define the edges of the bounding box
                const [minX, minY, maxX, maxY] = bbox;
                const edges = [
                    [[minX, minY], [maxX, minY]], // bottom edge
                    [[maxX, minY], [maxX, maxY]], // right edge
                    [[maxX, maxY], [minX, maxY]], // top edge
                    [[minX, maxY], [minX, minY]]  // left edge
                ];
    
                // Check intersection with each edge
                for (const edge of edges) {
                    if (doLinesIntersect(start, end, edge[0], edge[1])) {
                        return true;
                    }
                }
    
                return false;
            }
        </script>
    </head>
    <body onload="getMap()">
    </body>
    </html>
    

    If you made it this far, you will likely agree this is a decent amount of code. I wrote this in JavaScript but can easily be used in just about any programming language since there is no dependencies on other libraries. That said, if you plan to display the image on a web app, using the Azure Maps Web SDK would be a much better option and costs less than using the static image API.

    0 comments No comments

1 additional answer

Sort by: Most helpful
  1. Suwarna S Kale 3,946 Reputation points
    2025-07-25T02:00:48.7966667+00:00

    Hello Mike,

    Thank you for posting your question in the Microsoft Q&A forum. 

    When working with Azure Maps to display route imagery, the primary challenge lies in handling lengthy path coordinates that exceed URL limitations in the Static Maps API. The recommended approach involves first obtaining route data through the Azure Maps Route Directions API, then optimizing the path by either reducing waypoints to key locations or converting the coordinates into a compressed encoded polyline format.  

    For static map generation, use the Render V2 API (/map/static/png) with the simplified path to ensure the request remains within URL length constraints. Alternatively, for more flexibility, the Azure Maps Web SDK can dynamically render routes without such restrictions. If server-side processing is feasible, further optimize the path by removing redundant points while maintaining critical turns and shape accuracy. This method ensures efficient rendering while adhering to Azure Maps API limitations. For complex routes, consider splitting long paths into segments or using zoom-level-based simplification.

    Reference

    https://learn.microsoft.com/en-us/azure/azure-maps/tutorial-route-location

    https://learn.microsoft.com/en-us/azure/azure-maps/tutorial-prioritized-routes

    https://learn.microsoft.com/en-us/azure/azure-maps/migrate-get-static-map

    If the above answer helped, please do not forget to "Accept Answer" as this may help other community members to refer the info if facing a similar issue. Your contribution to the Microsoft Q&A community is highly appreciated. 

    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.