In an era where navigation apps dominate, there are still situations where printable driving directions are essential. These include:
- Road trip planning – ensuring you have a backup in case of connectivity issues.
- Event organization – providing guests with clear instructions.
- Traveling through areas with limited internet access – avoiding reliance on online maps.
This tutorial demonstrates how to generate printable driving directions using the Geoapify Routing API. By combining precise route calculations with static map visuals, you can create clear, user-friendly instructions for both online and offline use.
Explore the process in real-time with our JSFiddle demo—adjust parameters, view results, and experiment with route data as you follow along.
Key Features Covered in This Tutorial
- Get a Route with Instructions Details
- Generating a static map preview of the entire route.
- Creating a detailed list of turn-by-turn instructions.
- Producing static map previews for individual steps in the route.
1: Get a Route with Instructions Details
As a first step, you first need to calculate the route, including turn-by-turn instructions details. This is done using the Geoapify Routing API, which takes waypoints and returns route data in a structured format.
API Request URL Example
Here’s an example of the request URL to retrieve a route:
https://api.geoapify.com/v1/routing?waypoints=lat1,lon1|lat2,lon2&mode=drive&details=instructions,elevation&apiKey=YOUR_API_KEY
This request includes:
- Waypoints: The start and destination coordinates.
- Mode: The travel mode (e.g.,
drive
,walk
,bike
). - Details: Requesting
instructions
for detailed step-by-step navigation data.
Code Example to Fetch Route Data
The following code demonstrates how to make this API request and retrieve the route details:
const apiKey = "YOUR_API_KEY";
const url = `https://api.geoapify.com/v1/routing?waypoints=48.8588443,2.2943506|48.856614,2.3522219&mode=drive&details=instructions,elevation&apiKey=${apiKey}`;
fetch(url)
.then(response => response.json())
.then(geojson => {
console.log("Route Data:", geojson);
})
.catch(error => {
console.error("Error fetching route:", error);
});
Using @geoapify/route-directions
for an Interactive UI
In our JSFiddle example, we used the @geoapify/route-directions
package. This package provides a UI that allows users to select waypoints interactively and automatically fetches the calculated route. This simplifies the process by handling user input and API requests, returning a ready-to-use route when calculation is complete.
Once the route is obtained, the next step is to visualize it as a static map using an HTTP POST request.
2. Creating a Static Map Preview for the Entire Route
Displaying the entire route, which often consists of complex geometry, will not work with an HTTP GET request due to URL length limitations. When route geometry is encoded as query parameters, detailed paths with multiple waypoints can exceed this limit, leading to errors or truncated data.
To handle this, you can use an HTTP POST request with our Static Maps API, which allows sending route geometry in the request body without size constraints. This ensures that even long or detailed routes can be properly rendered.
The code below demonstrates how to request a map preview using an HTTP POST request with Geoapify Static Maps:
// Function to generate a static map preview of the route
function getMapPreview(geojson) {
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json"); // Set the request content type to JSON
// Add styling properties to the GeoJSON object for route visualization
geojson.properties.linecolor = '#6699ff'; // Set the route line color
geojson.properties.linewidth = '5'; // Set the route line width
// Define parameters for the static map request
const params = {
style: "osm-bright", // Map style
width: 800, // Map width in pixels
height: 250, // Map height in pixels
scaleFactor: 2, // Increase resolution for better quality
geojson: geojson, // Include the route GeoJSON data
markers: geojson.properties.waypoints.map(waypoint => ({
lat: waypoint.location[1], // Latitude of the waypoint
lon: waypoint.location[0], // Longitude of the waypoint
color: "#ff0000", // Marker color
size: "medium", // Marker size
type: "awesome" // Marker type (awesome icons from Geoapify)
}))
};
// Define request options for the fetch API
const requestOptions = {
method: "POST", // HTTP method
headers: myHeaders, // Headers for the request
body: JSON.stringify(params), // Stringify the parameters to include in the request body
redirect: "follow" // Handle redirects automatically
};
// Fetch the static map from Geoapify Static Maps API
fetch(`https://maps.geoapify.com/v1/staticmap?apiKey=${apiKey}`, requestOptions)
.then((response) => response.blob()) // Convert the response to a Blob object
.then((blob) => {
const reader = new FileReader();
reader.onload = function() {
// Set the image source of the route preview element to the base64 data URL
const mapPreview = document.getElementById("route-preview");
mapPreview.src = this.result;
};
reader.readAsDataURL(blob); // Convert the Blob to a data URL
})
.catch((error) => console.error(error)); // Log errors to the console
}
This code performs the following steps:
- Calls the Geoapify Static Maps API using an HTTP POST request.
- Sends the route geometry in the request body, avoiding URL length limitations.
- Defines visual parameters, including route color, line width, map dimensions, and waypoint markers.
- Receives the response as a blob (image data).
- Converts the blob to a data URL.
- Sets the data URL as the
src
of an<img>
element to display the static map preview.
3. Generating Turn-by-Turn Instructions
The Routing API returns route details along with its geometry. The route details are structured as legs, where each leg represents the segment between two waypoints. Each leg is further divided into steps, which describe individual maneuvers needed to follow the route:
Legs
- A leg represents the route segment between two waypoints.
- For example, if there are three waypoints (A, B, and C), the route will have two legs:
- Leg 1: From A to B
- Leg 2: From B to C
- Each leg contains detailed steps describing the maneuvers needed to navigate that segment of the route.
Steps
- A step describes a specific maneuver or turn along the route.
- For example:
- "Turn right onto Main St."
- "Drive south on 1st Ave."
- Each step includes:
from_index
andto_index
: Indicating the start and end coordinates in the geometry array.distance
: Length of the step (in meters).time
: Estimated time to complete the step (in seconds).instruction.text
: Text describing the maneuver.
Route Legs and Steps Example
{
"properties": {
"legs": [
{
"distance": 298,
"time": 40.948,
"steps": [
{
"from_index": 0,
"to_index": 1,
"distance": 40,
"time": 4.184,
"instruction": {
"text": "Drive south on Mousaiou."
}
},
{
"from_index": 1,
"to_index": 3,
"distance": 131,
"time": 19.313,
"instruction": {
"text": "Turn right onto Panou Mesi."
}
}
]
}
]
}
}
Generating Turn-by-turn Instructions
The following code generates HTML elements displaying route direction instructions based on the route object, specifically its legs and steps. It extracts navigation details from each leg and step, formatting them into a structured list for clear presentation:
// Function to generate turn-by-turn instructions for the route
function generateInstructions(geojson) {
const waypoints = routeDirections.getOptions().waypoints;
const instrictionContainer = document.getElementById("instructions");
instrictionContainer.innerHTML = ''; // Clear previous instructions
const isMetric = geojson.properties.distance_units === 'meters'; // Check if metric units are used
// Iterate over each leg of the route
geojson.properties.legs.forEach((leg, index) => {
const waypointsInfo = document.createElement("div");
waypointsInfo.classList.add("direction-waypoints");
// Display starting and ending waypoints for the leg
const from = `${waypoints[index].address || `${waypoints[index].lat} ${waypoints[index].lon}`}`;
const to = `${waypoints[index + 1].address || `${waypoints[index + 1].lat} ${waypoints[index + 1].lon}`}`;
const distance = toPrettyDistance(leg.distance, isMetric);
const time = toPrettyTime(leg.time);
waypointsInfo.textContent = `${distance}, ${time}`;
instrictionContainer.appendChild(waypointsInfo);
// Generate instructions for each step in the leg
leg.steps.forEach((step, stepIndex) => {
const instruction = document.createElement("div");
instruction.classList.add("direction-instruction");
// Add step number
const numberElement = document.createElement("div");
numberElement.classList.add("direction-instruction-number");
numberElement.innerHTML = `${stepIndex + 1}.`;
instruction.appendChild(numberElement);
// Add step description
const infoElement = document.createElement("div");
infoElement.classList.add("direction-instruction-info");
const textElement = document.createElement("div");
textElement.classList.add("direction-instruction-text");
let text = step.instruction.text;
// Highlight street names in the instruction text
if (step.instruction.streets) {
step.instruction.streets.forEach(street => {
text = text.split(street).join(`<b>${street}</b>`);
});
}
textElement.innerHTML = text;
infoElement.appendChild(textElement);
// Add post-transition instruction if it differs from the main instruction
if (step.instruction.post_transition_instruction && step.instruction.post_transition_instruction !== step.instruction.text) {
const textElementPost = document.createElement("div");
textElementPost.classList.add("direction-instruction-text-post");
textElementPost.textContent = step.instruction.post_transition_instruction;
infoElement.appendChild(textElementPost);
}
instruction.appendChild(infoElement);
instrictionContainer.appendChild(instruction);
// Add an image representing the maneuver
const imageElement = document.createElement("img");
imageElement.src = generateImageURL(index, step, geojson.geometry.coordinates);
imageElement.classList.add("direction-instruction-image");
instruction.appendChild(imageElement);
});
});
}
The code iterates through legs and steps of the route and generates HTML elements with specific IDs and classes to structure turn-by-turn instructions.
Generated HTML Elements
-
Waypoint Information (
.direction-waypoints
)- Shows starting and ending waypoints for each leg.
- If multiple legs exist, adds additional formatting for clarity.
- Detailed waypoint info (
.direction-waypoints-from-to
).
-
Step-by-Step Instructions
- Step number (
.direction-instruction-number
) – Orders the steps sequentially. - Instruction text (
.direction-instruction-text
) – Describes the maneuver (e.g., "Turn left onto Main St."). - Highlighted street names – Emphasizes street names within the instruction.
- Post-transition instructions (
.direction-instruction-text-post
) – Provides additional guidance after completing a maneuver. - Maneuver image (
.direction-instruction-image
) – Displays a visual representation of the turn.
- Step number (
4. Creating Static Maps for Individual Steps
In the code above, we call the generateImageURL()
function, which generates a Static Map to preview a step maneuver. This helps visualize turns, intersections, and other key navigation points directly within the instructions.
Now, let's take a closer look at how generateImageURL
works and how it constructs a Static Map for each step.
The following code demonstrates how to generate a preview image using the Geoapify Static Maps API. To enhance the map's usefulness, we will use bearing and pitch to adjust the map view, ensuring the angle and perspective are optimized for each step. Additionally, we will include geometries to display both the full route preview and the specific step preview on the map.
generateImageURL(legIndex, step, coordinates)
- Determines the maneuver's location and extracts step-specific geometries.
- Adjusts the map view using
bearing
andpitch
for better visualization. - Constructs the Geoapify Static Map API URL with route segments, maneuver indicators, and waypoints.
// Function to generate a URL for the maneuver preview image
function generateImageURL(legIndex, step, coordinates) {
let turnCoordinate = coordinates[legIndex][step.from_index];
let markerCoordinates = `${turnCoordinate[0]},${turnCoordinate[1]}`;
let style = "osm-bright";
const isStart = ["StartAt", "StartAtRight", "StartAtLeft"].includes(step.instruction.type);
const isFinish = ["DestinationReached", "DestinationReachedRight", "DestinationReachedLeft"].includes(step.instruction.type);
// Generate geometry data for different route segments and the maneuver
let relatedCoordinatesPast = getRelatedCoordinates(coordinates[legIndex], step, 'past');
let relatedCoordinatesNext = getRelatedCoordinates(coordinates[legIndex], step, 'next');
let manoeuvre = getRelatedCoordinates(coordinates[legIndex], step, 'manoeuvre');
let manoeuvreArrow = getRelatedCoordinates(coordinates[legIndex], step, 'manoeuvre-arrow');
let geometries = [];
if (!isStart) {
geometries.push(`polyline:${relatedCoordinatesPast};linewidth:5;linecolor:${encodeURIComponent('#ad9aad')}`);
}
if (!isFinish) {
geometries.push(`polyline:${relatedCoordinatesNext};linewidth:5;linecolor:${encodeURIComponent('#eb44ea')}`);
}
if (!isFinish) {
geometries.push(`polyline:${manoeuvre};linewidth:7;linecolor:${encodeURIComponent('#333333')};lineopacity:1`);
geometries.push(`polyline:${manoeuvre};linewidth:5;linecolor:${encodeURIComponent('#ffffff')};lineopacity:1`);
geometries.push(`polygon:${manoeuvreArrow};linewidth:1;linecolor:${encodeURIComponent('#333333')};lineopacity:1;fillcolor:${encodeURIComponent('#ffffff')};fillopacity:1`);
}
let bearing = getBearing(coordinates[legIndex], step) + 180;
let icon = isFinish ? `&marker=lonlat:${markerCoordinates};type:material;color:%23539de4;icon:flag-checkered;icontype:awesome;whitecircle:no` : '';
return `https://maps.geoapify.com/v1/staticmap?style=${style}&width=300&height=200&apiKey=${apiKey}&geometry=${geometries.join('|')}¢er=lonlat:${markerCoordinates}&zoom=16&scaleFactor=2&bearing=${bearing}&pitch=45${icon}`;
}
getBearing(coordinatesArray, step)
- Calculates the bearing (angle) between two points to correctly align the map view.
- Uses Turf.js Length to ensure a sufficient distance (at least 5 meters) between points.
- Uses Turf.js Bearing to get geographic bearing between two points.
// Function to calculate the bearing (angle) between two points
function getBearing(coordinatesArray, step) {
let currentCoordinateIndex = step.from_index; // Get the current step's index
let currentCoordinate = coordinatesArray[currentCoordinateIndex]; // Current coordinate
// Determine the index of the bearing coordinate (next or previous point)
let bearingCoordinateIndex = currentCoordinateIndex > 0 ? currentCoordinateIndex - 1 : currentCoordinateIndex + 1;
let bearingCoordinate = coordinatesArray[bearingCoordinateIndex];
// Loop to ensure the distance between points is at least 5 meters
while (true) {
if (turf.length(turf.lineString([bearingCoordinate, currentCoordinate])) >= 0.005 /* 5 meters */) {
break; // Stop if the distance is sufficient
}
// Break if reaching the first or last coordinate
if (bearingCoordinateIndex === 0 || bearingCoordinateIndex === coordinatesArray.length - 1) {
break;
}
// Adjust the index to check the next or previous coordinate
bearingCoordinateIndex = currentCoordinateIndex > 0 ? bearingCoordinateIndex - 1 : bearingCoordinateIndex + 1;
bearingCoordinate = coordinatesArray[bearingCoordinateIndex];
}
// Calculate the bearing using Turf.js
return currentCoordinateIndex > 0
? turf.bearing(turf.point(currentCoordinate), turf.point(bearingCoordinate))
: turf.bearing(turf.point(bearingCoordinate), turf.point(currentCoordinate));
}
getRelatedCoordinates(coordinatesArray, step, direction)
- Extracts relevant route segments for different visualization layers:
- Past route (
past
) – previous segment leading to the maneuver. - Next route (
next
) – upcoming segment after the maneuver. - Maneuver segment (
manoeuvre
) – highlights the turn itself. - Arrow (
manoeuvre-arrow
) – generates an arrow pointing to the maneuver direction.
- Past route (
- Uses Turf.js to clip the geometry within a bounding box to focus the preview on the maneuver area.
- Extracts relevant route segments for different visualization layers:
// Function to get related coordinates for different visualization purposes
function getRelatedCoordinates(coordinatesArray, step, direction) {
let currentCoordinateIndex = step.from_index; // Index of the current step's coordinate
const numberOfNextCoordinates = 20; // Number of coordinates to include in calculations
let coords;
if (direction === 'past') {
// Get past coordinates leading up to the current coordinate
coords = coordinatesArray.slice(
Math.max(0, currentCoordinateIndex - numberOfNextCoordinates),
currentCoordinateIndex + 1
);
} else if (direction === 'next') {
// Get next coordinates starting from the current coordinate
coords = coordinatesArray.slice(
currentCoordinateIndex,
currentCoordinateIndex + numberOfNextCoordinates + 1
);
} else if (direction === 'manoeuvre') {
// Get coordinates for a maneuver, clipped within a 20m view bounding box
const allCoords = coordinatesArray.slice(
Math.max(0, currentCoordinateIndex - numberOfNextCoordinates),
currentCoordinateIndex + numberOfNextCoordinates + 1
);
const viewBbox = turf.bbox(turf.circle(coordinatesArray[currentCoordinateIndex], 0.02));
let clipped = turf.bboxClip(turf.lineString(allCoords), viewBbox);
if (clipped.geometry.type === 'MultiLineString') {
// Handle multi-line geometries by finding the relevant segment
clipped = turf.lineString(
clipped.geometry.coordinates.find(lineCoords =>
turf.booleanContains(turf.lineString(lineCoords), turf.point(coordinatesArray[currentCoordinateIndex]))
)
);
}
// Clip the maneuver further to focus on the arrow area
const bbox10M = turf.bbox(turf.circle(clipped.geometry.coordinates[clipped.geometry.coordinates.length - 1], 0.01));
let clippedForArrow = turf.bboxClip(clipped, bbox10M);
if (clippedForArrow.geometry.type === 'MultiLineString') {
clippedForArrow = turf.lineString(clippedForArrow.geometry.coordinates[clippedForArrow.geometry.coordinates.length - 1]);
}
if (clipped.geometry.coordinates.length && clippedForArrow.geometry.coordinates.length) {
// Create a segment for the maneuver
const segment = turf.lineSlice(clipped.geometry.coordinates[0], clippedForArrow.geometry.coordinates[0], clipped);
coords = segment.geometry.coordinates;
}
} else {
// Generate coordinates for maneuver arrows
const allCoords = coordinatesArray.slice(
Math.max(0, currentCoordinateIndex - numberOfNextCoordinates),
currentCoordinateIndex + numberOfNextCoordinates + 1
);
const viewBbox = turf.bbox(turf.circle(coordinatesArray[currentCoordinateIndex], 0.02));
let clipped = turf.bboxClip(turf.lineString(allCoords), viewBbox);
if (clipped.geometry.type === 'MultiLineString') {
// Handle multi-line geometries
clipped = turf.lineString(
clipped.geometry.coordinates.find(lineCoords =>
turf.booleanContains(turf.lineString(lineCoords), turf.point(coordinatesArray[currentCoordinateIndex]))
)
);
}
// Clip for the arrow region
const bbox10M = turf.bbox(turf.circle(clipped.geometry.coordinates[clipped.geometry.coordinates.length - 1], 0.01));
let clippedForArrow = turf.bboxClip(clipped, bbox10M);
if (clippedForArrow.geometry.type === 'MultiLineString') {
clippedForArrow = turf.lineString(clippedForArrow.geometry.coordinates[clippedForArrow.geometry.coordinates.length - 1]);
}
const bearing = turf.bearing(
clippedForArrow.geometry.coordinates[0],
clippedForArrow.geometry.coordinates[clippedForArrow.geometry.coordinates.length - 1]
);
// Define polygon coordinates for the arrow
coords = [
clippedForArrow.geometry.coordinates[clippedForArrow.geometry.coordinates.length - 1],
turf.destination(clippedForArrow.geometry.coordinates[0], 0.005, bearing + 90).geometry.coordinates,
turf.destination(clippedForArrow.geometry.coordinates[0], 0.005, bearing - 90).geometry.coordinates,
clippedForArrow.geometry.coordinates[clippedForArrow.geometry.coordinates.length - 1]
];
}
// Format the coordinates as a string for the map
let result = [];
for (let coordinate of coords) {
result.push(`${coordinate[0]},${coordinate[1]}`);
}
return result.join(",");
}
The code generates a Static Map URL for maneuver previews, adjusting the map view and including route and step-specific geometries.
Interactive Demo: Generate Printable Route Directions
Try the interactive demo to explore how static map previews and turn-by-turn instructions are generated for a route.
How It Works
The demo is hosted on JSFiddle, providing a ready-to-use environment. Here’s what you can do:
-
Visualize the Entire Route:
- Observe a route preview generated using the route's geometry.
- Experiment with different routes and waypoint configurations.
-
View Turn-by-Turn Instructions:
- See dynamically generated instructions for each step in the route.
- Instructions include distance, time, and maneuver details.
-
Preview Individual Steps:
- Generate static maps centered on each maneuver.
- Maps are oriented towards the direction of travel and include visual cues like arrows and markers.
What You've Learned and Next Steps
Summary of Achievements
- Successfully created a static map preview for the full route with an HTTP POST request to the Static Maps API.
- Generated clear and formatted turn-by-turn instructions.
- Produced step-specific static map previews for maneuver visualization.
Practical Applications
- Directions ready for offline use, sharing, or printing.
- Suitable for logistics, travel planning, or apps needing printable outputs.
Extensions and Next Steps
- Add customization options for map styles or instruction formats.
- Explore interactivity for dynamic route adjustments or alternate routes.
- Incorporate additional data, such as landmarks or stops.
Integrating Geoapify’s Routing API and Static Maps API makes it easy to create high-quality driving directions and static maps. Take advantage of these powerful tools to enhance navigation solutions in your projects.
Try these techniques and explore Geoapify’s developer documentation to expand functionality and customize it for your needs.