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}¢er=${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.