// Initialize the Leaflet map object
// Create a map with the given HTML container ID and disable the default zoom control
var map = L.map('my-map', { zoomControl: false }).setView([48.1500327, 11.5753989], 10);
// Define the Geoapify API key
// Note: This API key is restricted to this demo. Obtain your own API key from https://myprojects.geoapify.com
var apiKey = "6dc7fb95a3b246cfa0f3bcef5ce9ed9a";
// Define the URL for map tiles, accounting for high-resolution (retina) displays
var mapURL = L.Browser.retina
? `https://maps.geoapify.com/v1/tile/{mapStyle}/{z}/{x}/{y}@2x.png?apiKey={apiKey}`
: `https://maps.geoapify.com/v1/tile/{mapStyle}/{z}/{x}/{y}.png?apiKey={apiKey}`;
// Add a tile layer to the map
// Use Geoapify's tile service and specify options such as attribution and max zoom level
L.tileLayer(mapURL, {
attribution: 'Powered by <a href="https://www.geoapify.com/" target="_blank">Geoapify</a> | <a href="https://openmaptiles.org/" rel="nofollow" target="_blank">© OpenMapTiles</a> <a href="https://www.openstreetmap.org/copyright" rel="nofollow" target="_blank">© OpenStreetMap</a> contributors',
apiKey: apiKey, // Include the API key for authentication
mapStyle: "osm-bright-smooth", // Specify the map style (find more styles at https://apidocs.geoapify.com/docs/maps/map-tiles/)
maxZoom: 20 // Set the maximum zoom level for the map
}).addTo(map);
// Add a custom zoom control to the map
// Position the zoom control at the bottom-right corner
L.control.zoom({ position: 'bottomright' }).addTo(map);
// Initialize arrays and variables for managing markers, route layers, and instruction markers
let markers = []; // Array to store markers added to the map
let routeLayer; // Layer for displaying the route on the map
let routeShadowLayer; // Shadow layer for the route (optional for styling or effects)
let instructionMarkers = []; // Array to store markers associated with route instructions
// Define a custom icon for markers
const markerIcon = L.icon({
iconUrl: `https://api.geoapify.com/v1/icon/?type=awesome&scaleFactor=2&color=%23ff4949&apiKey=${apiKey}`, // Icon generated using Geoapify Marker Icon API
iconSize: [31, 46], // Dimensions of the icon (width, height) in pixels
iconAnchor: [15.5, 42], // The anchor point of the icon (aligned to the marker's location on the map)
popupAnchor: [0, -45] // Position where the popup opens relative to the iconAnchor
});
// Initialize the Geoapify Route Directions tool
// This tool allows route generation and provides support for various transportation modes and options
const routeDirections = new directions.RouteDirections(
document.getElementById("route-directions"), // HTML element to display route directions
apiKey, // Geoapify API key
{
supportedModes: [
'walk', 'hike', 'scooter', 'motorcycle', 'drive',
'light_truck', 'medium_truck', 'truck', 'bicycle',
'mountain_bike', 'road_bike', 'bus'
], // Modes of transportation supported by the route directions tool
supportedOptions: ['highways', 'tolls', 'ferries'], // Options to avoid specific route types
elevation: true // Enable elevation data for supported modes (e.g., hiking or biking)
},
{
placeholder: "Enter an address here or click on the map" // Placeholder text for the input field
}
);
// Enable adding locations by clicking on the map
// When the map is clicked, add the clicked location (latitude and longitude) to the route directions
map.on("click", (event) => {
routeDirections.addLocation(event.latlng.lat, event.latlng.lng);
});
// Listen for changes in waypoints (locations added, removed, or reordered)
routeDirections.on('waypointChanged', (waypoint, reason) => {
// If the change is not due to adding a waypoint and a route is already displayed, clear the existing route
if (reason !== "added" && routeLayer) {
routeLayer.remove(); // Remove the main route layer
routeShadowLayer.remove(); // Remove the shadow layer (if present)
instructionMarkers.forEach(marker => marker.remove()); // Remove all instruction markers
instructionMarkers = []; // Reset the instruction markers array
}
// Clear the route instructions container
const instrictionContainer = document.getElementById("instructions");
instrictionContainer.innerHTML = '';
// Update map markers and UI elements
updateMarkers(); // Refresh the markers on the map
updateElementsVisibility(); // Show or hide elements based on the current state
});
// Listen for the completion of route calculation
routeDirections.on('routeCalculated', (geojson) => {
// Display the route visually on the map
visualizeRoute(geojson);
// Generate turn-by-turn instructions from the route data
generateInstructions(geojson);
// Generate and display a static map preview of the route
getMapPreview(geojson);
// Update the visibility of UI elements based on the calculated route
updateElementsVisibility();
});
// 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 => {
return {
"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) => {
var 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;
mapPreview.classList.remove("hidden"); // Show the map preview
};
reader.readAsDataURL(blob); // Convert the Blob to a data URL
})
.catch((error) => console.error(error)); // Log errors to the console
}
// Function to update the visibility of UI elements based on route and instructions
function updateElementsVisibility() {
const instructionsPlaceholder = document.getElementById("instructions-placeholder"); // Placeholder when no instructions are available
const mapPreview = document.getElementById("route-preview"); // Static map preview element
const instructions = document.getElementById("instructions"); // Route instructions container
const title = document.getElementById("driving-directions-title"); // Title
if (instructions.innerHTML.trim()) {
// If instructions are available, show the print button and hide the placeholder
instructionsPlaceholder.classList.add("hidden");
title.classList.remove("hidden");
} else {
// If no instructions, hide the map preview and print button, and show the placeholder
mapPreview.classList.add("hidden");
instructionsPlaceholder.classList.remove("hidden");
title.classList.add("hidden");
}
}
// Function to update map markers based on current waypoints
function updateMarkers() {
// Remove existing markers from the map
markers.forEach(marker => marker.remove());
markers = []; // Clear the markers array
const bounds = L.latLngBounds(); // Initialize bounds to adjust the map view dynamically
// Get current route options and filter valid waypoints (with latitude and longitude)
const options = routeDirections.getOptions();
options.waypoints.filter(waypoint => waypoint.lat && waypoint.lon).forEach(waypoint => {
// Extend map bounds to include the waypoint's location
bounds.extend([waypoint.lat, waypoint.lon]);
// Add a new marker for the waypoint and store it in the markers array
markers.push(L.marker([waypoint.lat, waypoint.lon], {
icon: markerIcon // Use the custom marker icon
}).addTo(map));
});
// Adjust the map view based on the markers
if (markers.length > 1 && bounds.isValid()) {
// If there are multiple markers, fit the map bounds to include all of them
map.fitBounds(bounds, { padding: [100, 100] });
} else if (markers.length) {
// If only one marker exists, center the map on that marker
map.panTo(markers[0].getLatLng());
}
}
// Function to visualize the calculated route on the map
function visualizeRoute(geojson) {
// Remove existing route layers and instruction markers from the map
if (routeLayer) {
routeLayer.remove(); // Remove the main route layer
routeShadowLayer.remove(); // Remove the shadow route layer
instructionMarkers.forEach(marker => marker.remove()); // Remove all instruction markers
instructionMarkers = []; // Clear the instruction markers array
}
// If the GeoJSON is invalid or missing properties, exit the function
if (!geojson || !geojson.properties) {
return;
}
// Create and add a shadow layer for the route (for visual enhancement)
routeShadowLayer = L.geoJSON(geojson, {
style: function (feature) {
return { color: '#0055ff', weight: 6 }; // Styling for the shadow layer
}
}).addTo(map);
// Create and add the main route layer
// Check more styling options at https://leafletjs.com/reference.html#path
routeLayer = L.geoJSON(geojson, {
style: function (feature) {
return { color: '#6699ff', weight: 4 }; // Styling for the route layer
}
}).addTo(map);
// Extract route points from GeoJSON geometry
const points = geojson.geometry.coordinates;
// Add markers for turn-by-turn instructions
geojson.properties.legs.forEach((leg, legIndex) => {
const legPoints = points[legIndex]; // Points corresponding to the current leg of the route
leg.steps.forEach(step => {
// Create a marker for each step with turn-by-turn instructions
// Check more styling options at https://leafletjs.com/reference.html#path
instructionMarkers.push(L.circleMarker(
[legPoints[step.from_index][1], legPoints[step.from_index][0]], // Latitude and longitude of the step
{
radius: 5, // Marker radius
fill: true, // Enable fill
fillOpacity: 1, // Fully opaque fill
fillColor: "#fff", // Fill color
color: '#0055ff', // Outline color
weight: 1 // Outline weight
}
).bindPopup(step.instruction.text).addTo(map)); // Add the marker with a popup showing the step instruction
});
});
}
// Function to generate turn-by-turn instructions for the route
function generateInstructions(geojson) {
// Mapping instruction types to corresponding icons
const type2icon = {
"StartAt": "navigation",
"StartAtRight": "navigation",
"StartAtLeft": "navigation",
"DestinationReached": "place",
"DestinationReachedRight": "place",
"DestinationReachedLeft": "place",
"Straight": "straight",
"SlightRight": "turn_slight_right",
"Right": "turn_right",
"SharpRight": "turn_right",
"TurnAroundRight": "u_turn_right",
"TurnAroundLeft": "u_turn_left",
"SharpLeft": "turn_left",
"Left": "turn_left",
"SlightLeft": "turn_slight_left",
"ExitRight": "turn_slight_right",
"ExitLeft": "turn_slight_left",
"StayRight": "straight",
"StayLeft": "straight",
"Merge": "merge",
"FerryEnter": "directions_boat",
"FerryExit": "directions_boat",
"MergeRight": "ramp_right",
"MergeLeft": "ramp_left",
"Roundabout": "roundabout"
};
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
// Add overall route summary if multiple legs exist
if (geojson.properties.legs.length > 1) {
const waypointsInfo = document.createElement("div");
waypointsInfo.classList.add("direction-waypoints");
const distance = toPrettyDistance(geojson.properties.distance, isMetric);
const time = toPrettyTime(geojson.properties.time);
waypointsInfo.textContent = `${distance}, ${time}`;
instrictionContainer.appendChild(waypointsInfo);
}
// 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}`;
// Add more detailed information for multi-leg routes
if (geojson.properties.legs.length > 1) {
waypointsInfo.classList.add("smaller");
const fromTo = document.createElement("div");
fromTo.classList.add("direction-waypoints-from-to");
fromTo.textContent = `(${from} - ${to})`;
waypointsInfo.appendChild(fromTo);
}
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 icon based on instruction type
const iconElement = document.createElement("div");
iconElement.classList.add("direction-instruction-icon");
if (type2icon[step.instruction.type]) {
addIcon(iconElement, type2icon[step.instruction.type]);
}
instruction.appendChild(iconElement);
// 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);
});
});
}
// 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}`;
}
// 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));
}
// 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(",");
}
// Function to add an SVG icon to a given DOM element
function addIcon(element, icon) {
// Icon definitions as SVG path or polygon data
const icons = {
navigation: "M12 2L4.5 20.29l.71.71L12 18l6.79 3 .71-.71z",
place: "M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z",
straight: "11,6.83 9.41,8.41 8,7 12,3 16,7 14.59,8.41 13,6.83 13,21 11,21",
turn_slight_right: "M12.34,6V4H18v5.66h-2V7.41l-5,5V20H9v-7.58c0-0.53,0.21-1.04,0.59-1.41l5-5H12.34z",
turn_right: "M17.17,11l-1.59,1.59L17,14l4-4l-4-4l-1.41,1.41L17.17,9L9,9c-1.1,0-2,0.9-2,2v9h2v-9L17.17,11z",
u_turn_right: "M6,9v12h2V9c0-2.21,1.79-4,4-4s4,1.79,4,4v4.17l-1.59-1.59L13,13l4,4l4-4l-1.41-1.41L18,13.17V9c0-3.31-2.69-6-6-6 S6,5.69,6,9z",
u_turn_left: "M18,9v12h-2V9c0-2.21-1.79-4-4-4S8,6.79,8,9v4.17l1.59-1.59L11,13l-4,4l-4-4l1.41-1.41L6,13.17V9c0-3.31,2.69-6,6-6 S18,5.69,18,9z",
turn_left: "M6.83,11l1.59,1.59L7,14l-4-4l4-4l1.41,1.41L6.83,9L15,9c1.1,0,2,0.9,2,2v9h-2v-9L6.83,11z",
turn_slight_left: "M11.66,6V4H6v5.66h2V7.41l5,5V20h2v-7.58c0-0.53-0.21-1.04-0.59-1.41l-5-5H11.66z",
merge: "M6.41,21L5,19.59l4.83-4.83c0.75-0.75,1.17-1.77,1.17-2.83v-5.1L9.41,8.41L8,7l4-4l4,4l-1.41,1.41L13,6.83v5.1 c0,1.06,0.42,2.08,1.17,2.83L19,19.59L17.59,21L12,15.41L6.41,21z",
directions_boat: "M20 21c-1.39 0-2.78-.47-4-1.32-2.44 1.71-5.56 1.71-8 0C6.78 20.53 5.39 21 4 21H2v2h2c1.38 0 2.74-.35 4-.99 2.52 1.29 5.48 1.29 8 0 1.26.65 2.62.99 4 .99h2v-2h-2zM3.95 19H4c1.6 0 3.02-.88 4-2 .98 1.12 2.4 2 4 2s3.02-.88 4-2c.98 1.12 2.4 2 4 2h.05l1.89-6.68c.08-.26.06-.54-.06-.78s-.34-.42-.6-.5L20 10.62V6c0-1.1-.9-2-2-2h-3V1H9v3H6c-1.1 0-2 .9-2 2v4.62l-1.29.42c-.26.08-.48.26-.6.5s-.15.52-.06.78L3.95 19zM6 6h12v3.97L12 8 6 9.97V6z",
ramp_right: "M11,21h2V6.83l1.59,1.59L16,7l-4-4L8,7l1.41,1.41L11,6.83V9c0,4.27-4.03,7.13-6,8.27l1.46,1.46 C8.37,17.56,9.9,16.19,11,14.7L11,21z",
ramp_left: "M13,21h-2V6.83L9.41,8.41L8,7l4-4l4,4l-1.41,1.41L13,6.83V9c0,4.27,4.03,7.13,6,8.27l-1.46,1.46 c-1.91-1.16-3.44-2.53-4.54-4.02L13,21z",
roundabout: "M 21.896702,16.807279 c -0.616921,-1.781843 -1.233842,-3.563685 -1.850763,-5.345528 -1.781843,0.616921 -3.563686,1.233841 -5.345529,1.850762 0.899536,0.436846 1.799073,0.873692 2.698609,1.310538 -1.11625,2.404166 -3.955561,3.812593 -6.545968,3.267515 -0.12287,0.655398 -0.245741,1.310795 -0.368611,1.966193 3.392477,0.709283 7.107172,-1.090998 8.636322,-4.204987 0.180625,-0.304366 0.638154,0.204973 0.942889,0.265315 0.611017,0.296731 1.222034,0.593461 1.833051,0.890192 z M 3.0679937,18.424041 c 1.8609148,0.304223 3.7218297,0.608445 5.5827445,0.912668 C 8.9549611,17.475794 9.2591839,15.61488 9.5634068,13.753965 8.7514802,14.337724 7.9395535,14.921484 7.1276269,15.505243 5.5430465,13.380359 5.6535648,10.212845 7.3644605,8.1929056 6.8462477,7.7732646 6.328035,7.3536235 5.8098222,6.9339825 3.573199,9.5815282 3.3913242,13.70547 5.4041365,16.531034 5.5860587,16.834627 4.9204584,16.995049 4.7225152,17.234472 4.171008,17.630995 3.6195009,18.027518 3.0679937,18.424041 Z M 11.169997,1.0313754 C 9.941056,2.4615011 8.712115,3.8916268 7.483174,5.3217525 c 1.4301257,1.228941 2.860251,2.4578821 4.290377,3.6868231 -0.07544,-0.99715 -0.150889,-1.9943 -0.226333,-2.99145 2.63923,-0.2459573 5.285628,1.4981573 6.118854,4.0107364 C 18.294206,9.8040536 18.922341,9.5802452 19.550475,9.3564368 18.455312,6.0681852 15.02962,3.7650111 11.569211,4.0115874 11.215296,4.0087627 11.425023,3.3570159 11.323735,3.0633399 11.272489,2.3860184 11.221243,1.7086969 11.169997,1.0313754 Z"
};
// Create an SVG element
var svgElement = document.createElementNS("http://www.w3.org/2000/svg", 'svg');
svgElement.setAttribute('viewBox', "0 0 24 24");
svgElement.setAttribute('height', "24");
// Add an SVG path or polygon element based on the icon data
if (icons[icon].startsWith("M")) {
var iconElement = document.createElementNS("http://www.w3.org/2000/svg", 'path');
iconElement.setAttribute("d", icons[icon]);
iconElement.setAttribute('fill', 'currentColor');
svgElement.appendChild(iconElement);
} else {
var iconElement = document.createElementNS("http://www.w3.org/2000/svg", 'polygon');
iconElement.setAttribute("points", icons[icon]);
iconElement.setAttribute('fill', 'currentColor');
svgElement.appendChild(iconElement);
}
// Append the created SVG to the target element
element.appendChild(svgElement);
}
// Function to format time in a human-readable format
function toPrettyTime(seconds) {
if (seconds === 0) return '0';
if (seconds < 120) return seconds + 's';
let hours = Math.floor(seconds / 3600);
let minutes = Math.floor((seconds - (hours * 3600)) / 60);
if (!hours) return minutes + 'min';
if (!minutes) return hours + 'h';
return hours + 'h ' + minutes + 'm';
}
// Function to format distance based on metric/imperial units
function toPrettyDistance(value, isMetric) {
if (!isMetric) {
// Format for imperial units
if (value >= 0.1) return `${value.toFixed(1)}mi`;
return `${Math.round(value * 5280)}feet`;
}
// Format for metric units
if (value > 10000) return `${(value / 1000).toFixed(1)}km`;
if (value > 5000) return `${(value / 1000).toFixed(1)}km`;
return `${Math.round(value)}m`;
}
<div class="demo-container">
<div class="map-container">
<div id="my-map"></div>
<div class="controls"><div id="route-directions"></div></div>
</div>
<div class="route-direction-to-print-container">
<div class="printable-driving-directions-title hidden" id="driving-directions-title">
<h1>Printable Driving Directions</h1>
</div>
<div class="instructions-placeholder" id="instructions-placeholder">
No waypoints have been selected. Please add at least two waypoints to
generate driving directions.
</div>
<div class="route-preview-container" id="route-preview-container">
<img class="route-preview hidden" id="route-preview" />
</div>
<div class="elevation-profile-container hidden" id="chart-container">
<canvas
id="route-elevation-chart"
style="width: 100%; height: 100%"
></canvas>
</div>
<div id="instructions"></div>
</div>
</div>
html,
body {
width: 100%;
height: 100%;
margin: 0;
}
body {
display: flex;
flex-direction: column;
}
.demo-container {
max-height: 100%;
}
.map-container {
flex: 1;
margin: 10px;
box-shadow: 0px 0px 5px 3px rgb(0 0 0 / 10%);
border-radius: 5px;
position: relative;
display: flex;
height: 400px;
}
.controls {
padding: 20px;
display: flex;
flex-direction: column;
position: absolute;
z-index: 2000;
background: white;
border-bottom-right-radius: 3px;
}
#instructions {
flex: 1;
max-width: 100%;
margin: auto;
}
#my-map {
flex: 1;
}
.route-direction-to-print-container {
padding: 10px;
padding-top: 20px;
overflow-y: auto;
display: block;
position: relative;
}
.direction-waypoints {
font-size: 18px;
font-weight: 600;
color: rgba(0, 0, 0, 0.8);
margin-bottom: 30px;
}
.direction-waypoints.smaller {
font-size: 16px;
font-weight: 500;
}
.direction-waypoints .direction-waypoints-from-to{
font-size: 12px;
font-weight: 400;
}
.direction-instruction {
display: flex;
flex-direction: row;
margin: 10px 0;
color: rgba(0, 0, 0, 0.8);
}
.direction-instruction-number {
margin-right: 20px;
}
.direction-instruction-icon {
min-width: 40px;
max-width: 40px;
display: flex;
}
.direction-instruction-image {
width: 300px;
height: 200px;
display: flex;
margin-left: auto;
}
.button {
height: 30px;
margin-left: auto;
background: grey;
border: none;
width: 100px;
border-radius: 4px;
color: #fff;
padding: 5px 10px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
align-items: center;
gap: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
position: absolute;
top: 10px;
right: 10px;
}
.button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.button:active {
transform: translateY(1px);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.button:focus {
outline: none;
box-shadow: 0 0 4px rgba(76, 175, 80, 0.8);
}
.hidden {
display: none;
}
.elevation-profile-container {
max-height: 250px;
min-height: 250px;
text-align: center;
}
.instructions-placeholder {
padding: 20px;
text-align: center;
color: #888;
}
.route-preview-container {
text-align: center;
}
.route-preview {
width: 100%;
}