Edit in JSFiddle

// 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('|')}&center=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%;
}