Skip to content

Route Optimization

Advanced SmartMaps GL Routing Isochrone Matrix Autocomplete Drag & Drop

This use case demonstrates a logistics route optimization tool built with SmartMaps. It features multi-stop route planning with drag-and-drop waypoint reordering, isochrone analysis for visualizing 15-minute service areas, an emergency simulation that uses a distance matrix to find the nearest available driver, and driver tracking animation along the calculated route. Address input is powered by the SmartMaps Autocomplete API with real-time suggestions.

How it works

The demo walks the user through a 4-step logistics workflow, with each step unlocking the next accordion section:

  1. Tour Planning -- The user enters addresses via the Autocomplete API, optionally adds intermediate stops (drag-and-drop reordering), selects a transport mode, and calculates the route. When "find best order" is checked, the Trip optimizer determines the most efficient waypoint sequence.
  2. Results & Analysis -- The route is drawn on the map with distance/duration summary. An isochrone analysis visualizes the 15-minute reachable service area around the destination.
  3. Emergency Simulation -- A simulated urgent delivery appears near the destination. Random driver positions are generated, and the Matrix API determines which driver can reach the pickup fastest.
  4. Driver Tracking -- The nearest driver's route is calculated and an animated truck marker follows the path with smooth camera tracking (zoom, pitch, and center interpolation).

Features & APIs

  • SmartMaps GL Map -- Interactive map rendering with custom markers, route layers, and 3D pitch/bearing support
  • Routing API (Route) -- Point-to-point route calculation with support for car, bicycle, and pedestrian speed profiles
  • Routing API (Trip) -- Optimized multi-stop tour planning that finds the best waypoint order
  • Routing API (Isochrone) -- 15-minute reachability polygons to visualize service coverage areas
  • Routing API (Matrix) -- Distance matrix computation to determine the nearest driver for emergency dispatch
  • Autocomplete API -- Real-time address search with typeahead suggestions for all waypoint inputs
  • Driver Animation -- Simulated live tracking that animates a truck icon along the route with smooth camera follow
  • Drag-and-Drop Waypoints -- Interactive reordering of intermediate stops in the planning panel

API endpoints used

Endpoint Purpose
POST /api_rst/v2/geojson/route (type ROUTE) Point-to-point routing between waypoints
POST /api_rst/v2/geojson/route (type TRIP) Multi-stop tour optimization (best order)
POST /api_rst/v2/geojson/route (type ISOCHRONE) Reachability polygon from a single point
POST /api_rst/v2/geojson/route (type MATRIX) Travel-time matrix between one origin and multiple destinations
SmartMaps Autocomplete JS SDK (v5) Address search with typeahead suggestions

Code

/*
 * =====================================================================
 * SmartMaps Logistics & Tour Optimizer
 * =====================================================================
 *
 * This demo showcases a 4-step logistics workflow using the SmartMaps
 * Routing API family:
 *
 *   1. TOUR PLANNING
 *      Users enter addresses via the Autocomplete API, optionally add
 *      intermediate stops with drag-and-drop reordering, and choose a
 *      transport mode. The route is calculated using the Routing API
 *      (type ROUTE) or the Trip optimizer (type TRIP) when "find best
 *      order" is checked.
 *
 *   2. RESULTS & ANALYSIS
 *      The computed route is displayed on the map with distance/duration
 *      summary. An isochrone analysis (type ISOCHRONE) visualizes the
 *      15-minute reachable service area around the last waypoint.
 *
 *   3. EMERGENCY SIMULATION
 *      A simulated urgent delivery appears near the destination. Using
 *      the Matrix API (type MATRIX), the system identifies which of the
 *      randomly placed drivers can reach the pickup fastest.
 *
 *   4. DRIVER TRACKING
 *      The nearest driver's route is calculated and an animated truck
 *      marker follows the route with smooth camera tracking (zoom, pitch,
 *      and center interpolation).
 *
 * API endpoints used:
 *   - Routing API: https://www.yellowmap.de/api_rst/v2/geojson/route
 *   - Autocomplete: SmartMaps Autocomplete JS SDK (v5)
 * =====================================================================
 */

// === CONFIGURATION & CONSTANTS =======================================

/** SmartMaps API key for authentication */
const apiKey = '[INSERT API-KEY]';

/** Isochrone reachability time in minutes */
const ISOCHRONE_TIME_MINUTES = 15;

/** Isochrone grid resolution (higher = more detailed polygon) */
const ISOCHRONE_GRID = "100";

/** Number of simulated drivers placed around the destination */
const DRIVER_COUNT = 10;

/** Radius in km within which drivers are randomly distributed */
const DRIVER_SPREAD_RADIUS_KM = 5;

/** Lat/lng offset for the urgent delivery marker from the last waypoint */
const URGENT_DELIVERY_OFFSET = 0.01;

/** Duration of the driver tracking animation in milliseconds */
const TRACKING_ANIMATION_DURATION_MS = 25000;

/** Target zoom level during driver tracking animation */
const TRACKING_TARGET_ZOOM = 15.5;

/** Target pitch (tilt) during driver tracking animation */
const TRACKING_TARGET_PITCH = 55;

/** Smoothing factor for camera interpolation (0-1, higher = smoother) */
const TRACKING_SMOOTHING = 0.98;

/** Approximate km per degree of latitude (1 degree latitude ~ 111.32 km) */
const KM_TO_DEGREES = 111.32;


// === MAP INITIALIZATION ==============================================

/**
 * Reads a CSS custom property from the document root.
 * @param {string} variable - CSS variable name (e.g. '--brand-primary')
 * @returns {string} The computed value of the CSS variable
 */
function getCssVariable(variable) {
    return getComputedStyle(document.documentElement).getPropertyValue(variable).trim();
}

const map = new smartmapsgl.Map({
    apiKey: apiKey,
    container: 'map',
    center: { lat: 49.021649, lng: 8.439330 },
    zoom: 6,
    style: smartmapsgl.MapStyle.ESSENTIAL
});


// === DOM ELEMENTS & STATE ============================================

const controlPanel = document.getElementById('control-panel');
const waypointsContainer = document.getElementById('waypoints-container');
const addWaypointBtn = document.getElementById('add-waypoint-btn');
const calculateBtn = document.getElementById('calculate-btn');
const isochroneBtn = document.getElementById('isochrone-btn');
const matrixBtn = document.getElementById('matrix-btn');
const optimizeCheckbox = document.getElementById('optimize-checkbox');
const transportButtons = document.querySelectorAll('.transport-modes button');
const resetViewBtn = document.getElementById('reset-view-btn');

/** @type {Array<{input: HTMLInputElement, coords: number[]|null, group: HTMLElement}>} */
let waypoints = [];
/** @type {smartmapsgl.Marker[]} */
let mapMarkers = [];
/** Currently selected routing speed profile */
let currentSpeedProfile = 'FAST';
/** Coordinates of simulated driver locations */
let dynamicDriverLocations = [];
/** Coordinates of the urgent delivery marker */
let urgentDeliveryLocation = null;
/** Info about the best (fastest) driver from the matrix result */
let bestDriverInfo = {};

/** requestAnimationFrame ID for the tracking animation */
let animationFrameId;
/** GeoJSON feature of the driver-to-delivery route */
let driverRouteGeoJSON = null;
/** The animated truck marker instance */
let truckMarker = null;
/** Timestamp when the tracking animation started */
let animationStartTime;


// === WAYPOINT MANAGEMENT =============================================

/**
 * Initializes autocomplete on a waypoint input field and registers
 * it in the waypoints array.
 * @param {HTMLInputElement} inputElement - The input element to attach autocomplete to
 */
async function initializeWaypointInput(inputElement) {
    const waypoint = { input: inputElement, coords: null, group: inputElement.parentElement };
    waypoints.push(waypoint);

    const autocomplete = await smartmaps.autocompleteService.createAutocomplete(inputElement, apiKey, {});
    autocomplete.addEventListener('selected', (e) => {
        waypoint.coords = e.detail.geojson.geometry.coordinates;
        checkCalculable();
    });
    inputElement.addEventListener('input', () => { waypoint.coords = null; checkCalculable(); });
}

/**
 * Adds a new intermediate waypoint input field between the first and
 * last stops. Attaches autocomplete, a remove button, and re-indexes
 * all placeholders.
 */
async function addWaypoint() {
    const newGroup = document.createElement('div');
    newGroup.className = 'input-group';
    newGroup.innerHTML = `<input class="waypoint-input" type="search" placeholder="Zwischenstopp" /><button class="remove-waypoint"><i class="material-icons">close</i></button>`;

    const lastInputGroup = waypointsContainer.querySelector('.input-group:last-of-type');
    waypointsContainer.insertBefore(newGroup, lastInputGroup);

    const newInput = newGroup.querySelector('input');
    const newIndex = waypoints.length - 1;
    const newWaypoint = { input: newInput, coords: null, group: newGroup };
    waypoints.splice(newIndex, 0, newWaypoint);

    const autocomplete = await smartmaps.autocompleteService.createAutocomplete(newInput, apiKey, {});
    autocomplete.addEventListener('selected', (e) => {
        newWaypoint.coords = e.detail.geojson.geometry.coordinates;
        checkCalculable();
    });
    newInput.addEventListener('input', () => { newWaypoint.coords = null; checkCalculable(); });

    newGroup.querySelector('.remove-waypoint').addEventListener('click', () => {
        waypoints = waypoints.filter(wp => wp !== newWaypoint);
        newGroup.remove();
        checkCalculable();
        updateWaypointPlaceholders();
        const activeHeader = document.querySelector('.accordion-header.active');
        if (activeHeader) {
            const body = activeHeader.nextElementSibling;
            body.style.maxHeight = body.scrollHeight + "px";
        }
    });

    updateWaypointPlaceholders();
    const plannerHeader = document.getElementById('header-planner');
    if(plannerHeader.classList.contains('active')) {
        const body = plannerHeader.nextElementSibling;
        body.style.maxHeight = body.scrollHeight + "px";
    }
}

/**
 * Enables or disables the "Calculate" button depending on whether
 * all waypoints have resolved coordinates.
 */
function checkCalculable() {
    calculateBtn.disabled = !waypoints.every(wp => wp.coords !== null);
}

/**
 * Updates placeholder text and draggable state for all waypoint input
 * groups. The first is always "Start / Depot", the last is always
 * "Zieladresse", and intermediates are numbered "Zwischenstopp N".
 */
function updateWaypointPlaceholders() {
    const inputGroups = waypointsContainer.querySelectorAll('.input-group');
    inputGroups.forEach((group, index) => {
        const input = group.querySelector('input');
        if (index === 0) {
            input.placeholder = 'Start / Depot';
            group.draggable = false;
        } else if (index === inputGroups.length - 1) {
            input.placeholder = 'Zieladresse';
            group.draggable = false;
        } else {
            input.placeholder = `Zwischenstopp ${index}`;
            group.draggable = true;
        }
    });
}


// === ROUTE CALCULATION ===============================================

/**
 * Builds a standard GeoJSON FeatureCollection request body for the
 * SmartMaps Routing API.
 * @param {string} type - Routing type: "ROUTE", "TRIP", "ISOCHRONE", or "MATRIX"
 * @param {Object} extraParams - Additional routing parameters to merge
 * @param {Array} features - GeoJSON Feature array for the request
 * @returns {Object} Complete request body ready for JSON.stringify
 */
function buildRoutingRequest(type, extraParams, features) {
    return {
        type: "FeatureCollection",
        routingparams: { type, ...extraParams },
        authentication: { channel: "roadtrip-demo" },
        crs: { type: "name", properties: { name: "urn:ogc:def:crs:OGC:1.3:CRS84" } },
        features
    };
}

/**
 * Returns responsive map padding that accounts for the control panel
 * position on mobile (bottom sheet) vs. desktop (left sidebar).
 * @returns {{top: number, bottom: number, left: number, right: number}} Padding in pixels
 */
function getMapPadding() {
    if (window.innerWidth <= 768) {
        const panel = document.getElementById('control-panel');
        // Panel height + 20px extra spacing
        const panelHeight = panel ? panel.offsetHeight : 300;
        return { top: 40, bottom: panelHeight + 20, left: 20, right: 20 };
    } else {
        // Desktop padding: accounts for the left-side panel
        return { top: 40, bottom: 40, left: 460, right: 40 };
    }
}

/**
 * Calculates a route or optimized trip from the entered waypoints.
 * Uses TRIP mode when the "find best order" checkbox is checked,
 * otherwise uses standard ROUTE mode. Draws the result on the map
 * and displays distance/duration in the results accordion.
 */
async function calculateRoute() {
    calculateBtn.disabled = true;
    calculateBtn.innerHTML = '<span>Berechne...</span><div class="loader"></div>';
    clearMap();

    const coords = waypoints.map(wp => wp.coords);
    const useTrip = optimizeCheckbox.checked;

    const url = `https://www.yellowmap.de/api_rst/v2/geojson/route?apiKey=${smartmapsgl.encodeString(apiKey)}`;
    const requestBody = buildRoutingRequest(
        useTrip ? "TRIP" : "ROUTE",
        { speedProfile: currentSpeedProfile, routingRoundTrip: false, isoLocale: "de-DE", coordFormatOut: "GEODECIMAL_POINT" },
        useTrip
            ? [{ type: "Feature", geometry: { type: "MultiPoint", coordinates: coords }, properties: {} }]
            : coords.map(c => ({ type: "Feature", geometry: { type: "Point", coordinates: c }, properties: {} }))
    );

    try {
        const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) });
        if (!response.ok) throw new Error(`API Error: ${response.status} - ${await response.text()}`);
        const data = await response.json();

        if (data.features && data.features.length > 0) {
            const routeFeature = data.features[0];
            const props = data.properties;
            document.getElementById('route-summary').innerHTML = `<strong>Distanz:</strong> ${(props.distance / 1000).toFixed(1)} km<br><strong>Dauer:</strong> ca. ${new Date(props.duration * 1000).toISOString().substr(11, 8)}`;

            map.addSource('route', { type: 'geojson', data: routeFeature });
            map.addLayer({ id: 'route-line', type: 'line', source: 'route', layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-color': getCssVariable('--brand-primary'), 'line-width': 6, 'line-opacity': 0.8 } });

            // When using TRIP mode, read back the optimized waypoint order
            let stops = coords;
            if (useTrip && routeFeature.properties && routeFeature.properties.waypoints) {
                stops = routeFeature.properties.waypoints.map(wp => wp.originalRequest.geometry.coordinates);
            }

            // Place numbered markers at each stop
            stops.forEach((coord, index) => {
                const el = document.createElement('div');
                el.className = 'custom-marker';
                el.textContent = index === 0 ? 'A' : (index === stops.length - 1 ? 'B' : index);
                mapMarkers.push(new smartmapsgl.Marker({ element: el }).setLngLat(coord).addTo(map));
            });

            // Fit the map to the route with responsive padding
            const bounds = new smartmapsgl.LngLatBounds();
            routeFeature.geometry.coordinates.forEach(coord => bounds.extend(coord));
            map.fitBounds(bounds, { padding: getMapPadding() });

            toggleAccordion(document.getElementById('header-results'), true);

        } else { throw new Error("No route found in the API response."); }
    } catch (error) {
        console.error("Route calculation error:", error);
        document.getElementById('route-summary').innerHTML = `<p style="color: var(--error-color);">Route konnte nicht berechnet werden.</p>`;
        toggleAccordion(document.getElementById('header-results'), true);
    } finally {
        calculateBtn.disabled = false;
        calculateBtn.innerHTML = '<i class="material-icons">route</i> Route berechnen';
    }
}


// === ISOCHRONE ANALYSIS ==============================================

/**
 * Calculates and displays an isochrone polygon around the last waypoint.
 * The isochrone shows the area reachable within ISOCHRONE_TIME_MINUTES
 * using the currently selected speed profile.
 */
isochroneBtn.addEventListener('click', async () => {
    const startCoords = waypoints[waypoints.length - 1].coords;
    if (!startCoords) return;

    isochroneBtn.disabled = true;
    isochroneBtn.innerHTML = '<span>Lade...</span><div class="loader"></div>';

    const url = `https://www.yellowmap.de/api_rst/v2/geojson/route?apiKey=${smartmapsgl.encodeString(apiKey)}`;
    const body = buildRoutingRequest(
        "ISOCHRONE",
        { timeInMinutes: ISOCHRONE_TIME_MINUTES, speedProfile: currentSpeedProfile, isochroneGrid: ISOCHRONE_GRID, coordFormatOut: "GEODECIMAL_POINT" },
        [{ type: "Feature", geometry: { type: "Point", coordinates: startCoords }, properties: {} }]
    );

    try {
        const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
        if (!response.ok) throw new Error(`API Error: ${response.status}`);
        const data = await response.json();

        if (data.features && data.features.length > 0) {
            const isochroneFeature = data.features[0];
            // Remove any existing isochrone layers before adding new ones
            if (map.getSource('isochrone')) { map.removeLayer('isochrone-outline'); map.removeLayer('isochrone-area'); map.removeSource('isochrone'); }
            map.addSource('isochrone', { type: 'geojson', data: isochroneFeature });

            // Insert isochrone layers below the route line so the route stays visible
            const beforeId = map.getLayer('route-line') ? 'route-line' : undefined;
            map.addLayer({ id: 'isochrone-area', type: 'fill', source: 'isochrone', paint: { 'fill-color': getCssVariable('--brand-secondary'), 'fill-opacity': 0.3 } }, beforeId);
            map.addLayer({ id: 'isochrone-outline', type: 'line', source: 'isochrone', paint: { 'line-color': '#ffffff', 'line-width': 2, 'line-opacity': 0.9 } }, beforeId);

            // Fit the map to the isochrone bounds with responsive padding
            const bounds = new smartmapsgl.LngLatBounds();
            isochroneFeature.geometry.coordinates[0].forEach(coord => bounds.extend(coord));
            map.fitBounds(bounds, { padding: getMapPadding(), duration: 1000 });

            toggleAccordion(document.getElementById('header-simulation'), true);
        } else { console.error("Isochrone response contains no features."); }
    } catch (error) { console.error("Isochrone calculation error:", error);
    } finally {
        isochroneBtn.disabled = false;
        isochroneBtn.innerHTML = '<i class="material-icons">map</i> 15-Min Servicegebiet';
    }
});


// === MATRIX / EMERGENCY DISPATCH =====================================

/**
 * Simulates an emergency dispatch scenario. Places random driver markers
 * and an urgent delivery marker on the map, then uses the Matrix API to
 * find the fastest driver. Results are displayed in the tracking accordion.
 */
matrixBtn.addEventListener('click', async () => {
    const destination = waypoints[waypoints.length - 1].coords;
    if (!destination) return;

    // Remove any existing driver/urgent markers
    mapMarkers.filter(m => m.getElement().classList.contains('driver-marker') || m.getElement().classList.contains('urgent-marker')).forEach(m => m.remove());
    mapMarkers = mapMarkers.filter(m => !m.getElement().classList.contains('driver-marker') && !m.getElement().classList.contains('urgent-marker'));

    // Generate random driver positions and the urgent delivery location
    dynamicDriverLocations = generateDriverLocations(destination, DRIVER_COUNT, DRIVER_SPREAD_RADIUS_KM);
    urgentDeliveryLocation = [destination[0] + URGENT_DELIVERY_OFFSET, destination[1] + URGENT_DELIVERY_OFFSET];

    // Add driver markers to the map
    dynamicDriverLocations.forEach((coords, i) => {
        const el = document.createElement('div');
        el.className = 'custom-marker driver-marker';
        el.textContent = `F${i+1}`;
        mapMarkers.push(new smartmapsgl.Marker({ element: el }).setLngLat(coords).addTo(map));
    });

    // Add urgent delivery marker with pulsing animation
    const el = document.createElement('div');
    el.className = 'custom-marker urgent-marker';
    el.textContent = `!`;
    mapMarkers.push(new smartmapsgl.Marker({ element: el }).setLngLat(urgentDeliveryLocation).addTo(map));

    // Call the Matrix API to find travel times from the urgent location to all drivers
    const url = `https://www.yellowmap.de/api_rst/v2/geojson/route?apiKey=${smartmapsgl.encodeString(apiKey)}`;
    const body = buildRoutingRequest(
        "MATRIX",
        { speedProfile: "FAST" },
        [
            { type: "Feature", geometry: { type: "Point", coordinates: urgentDeliveryLocation }, properties: { type: "StartPoint" } },
            { type: "Feature", geometry: { type: "MultiPoint", coordinates: dynamicDriverLocations }, properties: {} }
        ]
    );
    const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
    const data = await response.json();

    const resultContainer = document.getElementById('matrix-result');

    if (data.features?.[0]?.properties?.routingDestinations) {
        const results = data.features[0].properties.routingDestinations;
        if (results.length > 0) {
            // Find the driver with the shortest travel time
            const best = results.reduce((prev, curr) => prev.timeInSeconds < curr.timeInSeconds ? prev : curr);
            const bestDriverIndex = results.indexOf(best);
            bestDriverInfo = {
                index: bestDriverIndex,
                coords: dynamicDriverLocations[bestDriverIndex],
                time: Math.round(best.timeInSeconds / 60)
            };

            resultContainer.innerHTML = `Ergebnis: Fahrer ${bestDriverInfo.index + 1} ist am schnellsten! (ca. ${bestDriverInfo.time} Min.)
            <button id="track-driver-btn" class="action-button tertiary-button"><i class="material-icons">track_changes</i> Fahrer verfolgen</button>`;
            document.getElementById('track-driver-btn').addEventListener('click', startDriverAnimation);
        } else { resultContainer.textContent = 'Matrix calculation: no results.'; }
    } else { resultContainer.textContent = 'Matrix calculation failed.'; }

    toggleAccordion(document.getElementById('header-tracking'), true);
});


// === DRIVER TRACKING ANIMATION =======================================

/**
 * Starts the driver tracking animation. Fetches a route from the best
 * driver to the urgent delivery location, hides the control panel, and
 * begins the frame-by-frame truck animation.
 */
async function startDriverAnimation() {
    const url = `https://www.yellowmap.de/api_rst/v2/geojson/route?apiKey=${smartmapsgl.encodeString(apiKey)}`;
    const requestBody = buildRoutingRequest(
        "ROUTE",
        { speedProfile: "FAST", routingRoundTrip: false, isoLocale: "de-DE", coordFormatOut: "GEODECIMAL_POINT" },
        [
            { type: "Feature", geometry: { type: "Point", coordinates: bestDriverInfo.coords }, properties: {} },
            { type: "Feature", geometry: { type: "Point", coordinates: urgentDeliveryLocation }, properties: {} }
        ]
    );

    const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) });
    if (!response.ok) {
        console.error("Route calculation error for tracking:", await response.text());
        return;
    }
    const data = await response.json();
    if (!data.features || data.features.length === 0) {
        console.error("Could not fetch route for driver tracking.");
        return;
    }
    driverRouteGeoJSON = data.features[0];

    // Hide the control panel with a slide-out animation
    controlPanel.style.opacity = 0;
    controlPanel.style.transform = 'translateX(-100%)';
    setTimeout(() => controlPanel.style.display = 'none', 500);
    resetViewBtn.style.display = 'flex';

    // Draw the driver route as a dashed line
    clearMap(true);
    if (map.getSource('driver-route')) {
        map.removeLayer('driver-route-line');
        map.removeSource('driver-route');
    }
    map.addSource('driver-route', { type: 'geojson', data: driverRouteGeoJSON });
    map.addLayer({ id: 'driver-route-line', type: 'line', source: 'driver-route', layout: {}, paint: { 'line-color': getCssVariable('--success-color'), 'line-width': 5, 'line-dasharray': [0, 2] } });

    // Create the truck marker at the driver's starting position
    const truckEl = document.createElement('div');
    truckEl.className = 'truck-marker';
    truckMarker = new smartmapsgl.Marker({ element: truckEl, anchor: 'center' }).setLngLat(bestDriverInfo.coords).addTo(map);

    animateDriver(0);
}

/**
 * Animation loop that moves the truck marker along the route coordinates.
 * Uses linear interpolation between route points and smoothly transitions
 * the camera center, zoom, and pitch toward their target values.
 * @param {DOMHighResTimeStamp} timestamp - The current animation frame timestamp
 */
function animateDriver(timestamp) {
    if (animationFrameId === null) return;
    if (!animationStartTime) animationStartTime = timestamp;

    const progress = Math.min((timestamp - animationStartTime) / TRACKING_ANIMATION_DURATION_MS, 1);
    const routeCoords = driverRouteGeoJSON.geometry.coordinates;

    // Interpolate position along the route polyline
    const exactIndex = progress * (routeCoords.length - 1);
    const startIndex = Math.floor(exactIndex);
    const endIndex = Math.min(startIndex + 1, routeCoords.length - 1);
    const segmentProgress = exactIndex - startIndex;
    const startPos = routeCoords[startIndex];
    const endPos = routeCoords[endIndex];
    const currentLng = startPos[0] + (endPos[0] - startPos[0]) * segmentProgress;
    const currentLat = startPos[1] + (endPos[1] - startPos[1]) * segmentProgress;
    const currentPos = [currentLng, currentLat];

    truckMarker.setLngLat(currentPos);

    // Smooth camera follow using exponential smoothing
    const smoothingWeight = 1 - (1 - TRACKING_SMOOTHING) * 5; // Center follows faster than zoom/pitch
    const smoothedCenter = new smartmapsgl.LngLat(
        map.getCenter().lng * smoothingWeight + currentPos[0] * (1 - smoothingWeight),
        map.getCenter().lat * smoothingWeight + currentPos[1] * (1 - smoothingWeight)
    );
    map.setCenter(smoothedCenter);

    // Gradually transition zoom and pitch toward target values
    map.setZoom(map.getZoom() * TRACKING_SMOOTHING + TRACKING_TARGET_ZOOM * (1 - TRACKING_SMOOTHING));
    map.setPitch(map.getPitch() * TRACKING_SMOOTHING + TRACKING_TARGET_PITCH * (1 - TRACKING_SMOOTHING));

    if (progress < 1) {
        animationFrameId = requestAnimationFrame(animateDriver);
    } else {
        animationStartTime = null;
        animationFrameId = null;
    }
}


// === UTILITY FUNCTIONS ===============================================

/**
 * Generates random driver locations distributed uniformly within a
 * circle of the given radius around a center point.
 * @param {number[]} center - [longitude, latitude] of the center point
 * @param {number} count - Number of driver locations to generate
 * @param {number} radius - Spread radius in kilometers
 * @returns {number[][]} Array of [longitude, latitude] coordinate pairs
 */
function generateDriverLocations(center, count, radius) {
    const [lon, lat] = center;
    const locations = [];
    const radiusInDegrees = radius / KM_TO_DEGREES;
    for (let i = 0; i < count; i++) {
        const angle = Math.random() * 2 * Math.PI;
        // sqrt ensures uniform distribution within the circle
        const distance = Math.sqrt(Math.random()) * radiusInDegrees;
        const driverLon = lon + (distance * Math.cos(angle)) / Math.cos(lat * Math.PI / 180);
        const driverLat = lat + distance * Math.sin(angle);
        locations.push([driverLon, driverLat]);
    }
    return locations;
}

/**
 * Removes all map layers, sources, and markers. Cancels any running
 * animation. When softClear is true, only the driver tracking layer
 * and truck marker are removed (route + isochrone layers are preserved).
 * @param {boolean} [softClear=false] - If true, preserve route/isochrone layers
 */
function clearMap(softClear = false) {
    if (animationFrameId) {
        cancelAnimationFrame(animationFrameId);
        animationFrameId = null;
        animationStartTime = null;
    }
    if (truckMarker) truckMarker.remove();

    if (map.getSource('driver-route')) { map.removeLayer('driver-route-line'); map.removeSource('driver-route'); }

    if (!softClear) {
        if (map.getSource('route')) { map.removeLayer('route-line'); map.removeSource('route'); }
        if (map.getSource('isochrone')) { map.removeLayer('isochrone-outline'); map.removeLayer('isochrone-area'); map.removeSource('isochrone'); }
        mapMarkers.forEach(marker => marker.remove());
        mapMarkers = [];
        resetAccordions();
    }
}

/**
 * Resets the entire view: clears the map, restores the control panel,
 * hides the reset button, and flies back to the default map position.
 */
function resetView() {
    clearMap();
    controlPanel.style.display = 'flex';
    setTimeout(() => {
        controlPanel.style.opacity = 1;
        controlPanel.style.transform = 'translateX(0)';
    }, 10);
    resetViewBtn.style.display = 'none';
    map.flyTo({ center: { lat: 49.021649, lng: 8.439330 }, zoom: 6, pitch: 0, bearing: 0 });
}
resetViewBtn.addEventListener('click', resetView);


// === ACCORDION UI LOGIC ==============================================

/**
 * Toggles an accordion section open or closed. Closes all other open
 * sections first (single-open behavior). When forceOpen is true, the
 * section is enabled (if disabled) and opened regardless of current state.
 * @param {HTMLElement} header - The accordion header button element
 * @param {boolean} [forceOpen=false] - Force-open the section and enable it if disabled
 */
function toggleAccordion(header, forceOpen = false) {
    if (header.disabled && !forceOpen) return;

    if (header.disabled) {
        header.disabled = false;
    }

    const body = header.nextElementSibling;
    const isActive = header.classList.contains('active');

    // Close all other open accordions before opening a new one
    if (!isActive || forceOpen) {
        document.querySelectorAll('.accordion-header.active').forEach(activeHeader => {
            if (activeHeader !== header) {
                activeHeader.classList.remove('active');
                activeHeader.nextElementSibling.style.maxHeight = null;
            }
        });
    }

    if (!isActive || forceOpen) {
        header.classList.add('active');
        body.style.maxHeight = body.scrollHeight + "px";
    } else {
        header.classList.remove('active');
        body.style.maxHeight = null;
    }
}

/**
 * Resets all accordion sections to their initial state: closes everything,
 * disables sections 2-4, and opens section 1 (Tour Planning).
 */
function resetAccordions() {
    document.querySelectorAll('.accordion-header').forEach((header, index) => {
        header.classList.remove('active');
        header.nextElementSibling.style.maxHeight = null;
        if (index > 0) {
            header.disabled = true;
        }
    });
    toggleAccordion(document.getElementById('header-planner'), true);
}

document.querySelectorAll('.accordion-header').forEach(header => {
    header.addEventListener('click', () => toggleAccordion(header));
});


// === DRAG & DROP =====================================================

/** @type {HTMLElement|null} The DOM element currently being dragged */
let draggedItem = null;

/**
 * Handles dragstart on waypoint input groups. Only intermediate stops
 * (draggable elements) can be dragged.
 */
waypointsContainer.addEventListener('dragstart', (e) => {
    if (e.target.classList.contains('input-group') && e.target.draggable) {
        draggedItem = e.target;
        setTimeout(() => e.target.classList.add('dragging'), 0);
    }
});

/** Handles dragend: removes the dragging visual state. */
waypointsContainer.addEventListener('dragend', () => {
    if (draggedItem) {
        draggedItem.classList.remove('dragging');
        draggedItem = null;
    }
});

/**
 * Handles dragover: repositions the dragged element in the DOM based
 * on the cursor's vertical position. Prevents dragging above the first
 * waypoint (start) or below the last (destination).
 */
waypointsContainer.addEventListener('dragover', (e) => {
    e.preventDefault();
    const draggedEl = document.querySelector('.dragging');
    if (!draggedEl) return;

    const afterElement = getDragAfterElement(waypointsContainer, e.clientY);

    if (afterElement === undefined) {
        waypointsContainer.insertBefore(draggedEl, waypointsContainer.lastElementChild);
    } else {
        // Prevent dropping before the first element (start waypoint)
        if (afterElement === waypointsContainer.firstElementChild) {
            return;
        }
        waypointsContainer.insertBefore(draggedEl, afterElement);
    }
});

/**
 * Handles drop: synchronizes the waypoints array order with the new
 * DOM order after a drag-and-drop reorder operation.
 */
waypointsContainer.addEventListener('drop', (e) => {
    e.preventDefault();
     if (draggedItem) {
        const newOrderWaypoints = [];
        const domInputGroups = Array.from(waypointsContainer.querySelectorAll('.input-group'));

        domInputGroups.forEach(group => {
            const waypointObject = waypoints.find(wp => wp.group === group);
            if (waypointObject) {
                newOrderWaypoints.push(waypointObject);
            }
        });

        waypoints = newOrderWaypoints;
        updateWaypointPlaceholders();
        checkCalculable();
    }
});

/**
 * Determines which element the dragged item should be inserted before,
 * based on the vertical cursor position relative to each child's midpoint.
 * @param {HTMLElement} container - The waypoints container
 * @param {number} y - The current cursor Y position
 * @returns {HTMLElement|undefined} The element to insert before, or undefined for end
 */
function getDragAfterElement(container, y) {
    const draggableElements = [...container.querySelectorAll('.input-group:not(.dragging)')];
    return draggableElements.reduce((closest, child) => {
        const box = child.getBoundingClientRect();
        const offset = y - box.top - box.height / 2;
        if (offset < 0 && offset > closest.offset) {
            return { offset: offset, element: child };
        } else {
            return closest;
        }
    }, { offset: Number.NEGATIVE_INFINITY }).element;
}


// === INITIALIZATION ==================================================

// Initialize autocomplete on all existing waypoint inputs
document.querySelectorAll('.waypoint-input').forEach(input => initializeWaypointInput(input));

// Wire up button event listeners
addWaypointBtn.addEventListener('click', addWaypoint);
calculateBtn.addEventListener('click', calculateRoute);

// Transport mode toggle buttons
transportButtons.forEach(button => {
    button.addEventListener('click', () => {
        transportButtons.forEach(btn => btn.classList.remove('active'));
        button.classList.add('active');
        currentSpeedProfile = button.dataset.profile;
    });
});

// Toggle the "optimize" checkbox when clicking anywhere on the options row
document.querySelector('.options').addEventListener('click', (e) => {
    if (e.target.id !== 'optimize-checkbox') {
        optimizeCheckbox.checked = !optimizeCheckbox.checked;
    }
});

// Set initial accordion and placeholder state
resetAccordions();
updateWaypointPlaceholders();
<body>

    <div id="map"></div>
    <button id="reset-view-btn"><i class="material-icons">arrow_back</i> Zurück zur Übersicht</button>
    <div class="control-panel" id="control-panel">
        <h1>Logistik & Touren-Optimierer</h1>

        <div id="accordion-container">
            <div class="accordion-item">
                <button class="accordion-header" id="header-planner">
                    <h2>1. Tour planen</h2>
                    <span class="info-icon">
                        <i class="material-icons">info</i>
                        <span class="tooltip">Nutzt die <strong>Autocomplete API</strong> und <strong>Routing API (Trip/Route)</strong>.</span>
                    </span>
                    <i class="material-icons chevron">expand_more</i>
                </button>
                <div class="accordion-body" id="body-planner">
                    <div class="accordion-content">
                        <div id="waypoints-container">
                            <div class="input-group"><input class="waypoint-input" type="search" placeholder="Start / Depot" /></div>
                            <div class="input-group"><input class="waypoint-input" type="search" placeholder="Zieladresse" /></div>
                        </div>
                        <button id="add-waypoint-btn"><i class="material-icons">add</i> Zwischenstopp</button>
                        <div class="options">
                            <input type="checkbox" id="optimize-checkbox">
                            <label for="optimize-checkbox">Beste Reihenfolge finden</label>
                        </div>
                        <div class="transport-modes">
                            <button class="active" data-profile="FAST"><i class="material-icons">directions_car</i><span>PKW</span></button>
                            <button data-profile="BICYCLE"><i class="material-icons">directions_bike</i><span>Rad</span></button>
                            <button data-profile="PEDESTRIAN"><i class="material-icons">directions_walk</i><span>Fuß</span></button>
                        </div>
                        <button id="calculate-btn" class="action-button" disabled><i class="material-icons">route</i> Route berechnen</button>
                    </div>
                </div>
            </div>

            <div class="accordion-item">
                <button class="accordion-header" id="header-results" disabled>
                    <h2>2. Ergebnis & Analyse</h2>
                    <span class="info-icon">
                        <i class="material-icons">info</i>
                        <span class="tooltip">Nutzt die <strong>Routing API (Isochrone)</strong>.</span>
                    </span>
                    <i class="material-icons chevron">expand_more</i>
                </button>
                <div class="accordion-body" id="body-results">
                    <div class="accordion-content">
                        <div id="route-summary"></div>
                        <button id="isochrone-btn" class="action-button secondary-button"><i class="material-icons">map</i>15-Min Servicegebiet</button>
                    </div>
                </div>
            </div>

            <div class="accordion-item">
                <button class="accordion-header" id="header-simulation" disabled>
                    <h2>3. Notfall-Simulation</h2>
                    <span class="info-icon">
                        <i class="material-icons">info</i>
                        <span class="tooltip">Nutzt die <strong>Routing API (Matrix)</strong>.</span>
                    </span>
                    <i class="material-icons chevron">expand_more</i>
                </button>
                <div class="accordion-body" id="body-simulation">
                    <div class="accordion-content">
                        <p>Ein Eilauftrag kommt rein. Welcher Fahrer ist am schnellsten da?</p>
                        <button id="matrix-btn" class="action-button secondary-button"><i class="material-icons">groups</i> Nächsten Fahrer finden</button>
                    </div>
                </div>
            </div>

            <div class="accordion-item">
                <button class="accordion-header" id="header-tracking" disabled>
                    <h2>4. Fahrer-Tracking</h2>
                    <span class="info-icon">
                        <i class="material-icons">info</i>
                        <span class="tooltip">Nutzt die <strong>Map Match API</strong> (simuliert durch Routing).</span>
                    </span>
                    <i class="material-icons chevron">expand_more</i>
                </button>
                <div class="accordion-body" id="body-tracking">
                    <div class="accordion-content" id="matrix-result"></div>
                </div>
            </div>
        </div>
    </div>

</body>
/* =================================================================== */
/* BASE STYLES (integrated from smartmaps-demo-styles.css)             */
/* =================================================================== */

:root {
    --brand-primary: #18345c;
    --brand-secondary: #3498db;
    --brand-accent: #f6b80c;
    --text-primary: #111827;
    --text-secondary: #374151;
    --text-light: #6b7280;
    --surface-bg: #f4f7fa;
    --surface-panel: #ffffff;
    --border-color: #e5e7eb;
    --success-color: #10b981;
    --error-color: #ef4444;
}

body {
    margin: 0;
    padding: 0;
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    background-color: var(--surface-bg);
    color: var(--text-primary);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

#map {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 100%;
    height: 100%;
}

.loader {
    border: 4px solid rgba(0,0,0,0.1);
    border-top: 4px solid var(--brand-secondary);
    border-radius: 50%;
    width: 20px;
    height: 20px;
    animation: spin 1s linear infinite;
    margin: 0 auto;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }

.info-icon {
    position: relative;
    display: inline-flex;
    align-items: center;
    cursor: pointer;
    color: #9ca3af;
}
.info-icon .tooltip {
    visibility: hidden; width: 260px; background-color: #1f2937;
    color: #fff; text-align: left; border-radius: 6px; padding: 12px;
    position: absolute; z-index: 100; opacity: 0; transition: opacity 0.3s;
    font-size: 0.85rem; line-height: 1.5; font-weight: 400;
    box-shadow: 0 4px 12px rgba(0,0,0,0.15); bottom: 130%; right: 0;
    left: auto; transform: translateX(0);
}
.info-icon .tooltip::after {
    content: ""; position: absolute; top: 100%; right: 10px; left: auto;
    margin-left: 0; border-width: 5px; border-style: solid;
    border-color: #1f2937 transparent transparent transparent;
}
.info-icon:hover .tooltip { visibility: visible; opacity: 1; }
.info-icon .tooltip strong { color: var(--brand-secondary); }

/* =================================================================== */
/* DEMO-SPECIFIC STYLES                                                */
/* =================================================================== */
h1 {
    background: var(--brand-primary);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    font-size: 1.75rem;
}

.control-panel { display: flex; flex-direction: column; }
.accordion-item { border-bottom: 1px solid var(--border-color); }
.accordion-item:last-child { border-bottom: none; }
.accordion-header {
    background: none; border: none; width: 100%; text-align: left;
    padding: 18px 4px; cursor: pointer; display: flex;
    justify-content: space-between; align-items: center;
    transition: background-color 0.2s;
}
.accordion-header:hover { background-color: #f9fafb; }
.accordion-header h2 {
    font-size: 1.1em; font-weight: 500; margin: 0;
    color: var(--text-secondary); border: none; padding: 0;
    display: flex; align-items: center; gap: 8px;
}
.accordion-header .info-icon { margin-left: auto; margin-right: 10px; }
.accordion-header .chevron { transition: transform 0.3s ease; }
.accordion-header.active .chevron { transform: rotate(180deg); }
.accordion-header[disabled] { cursor: not-allowed; background-color: #f9fafb; }
.accordion-header[disabled] h2 { color: #9ca3af; }
.accordion-body { max-height: 0; overflow: hidden; transition: max-height 0.4s ease-out; }
.accordion-content { padding: 1px 0 18px 0; }

.input-group {
    margin-bottom: 12px; position: relative; padding-left: 10px;
    border-left: 4px solid transparent;
}
.input-group[draggable="true"] { cursor: move; border-left: 4px solid #d1d5db; }
.input-group.dragging { opacity: 0.4; background: #eef2f7; }
.waypoint-input {
    width: 100%; padding: 10px; box-sizing: border-box; font-size: 1em;
    border: 1px solid #d1d5db; border-radius: 6px;
    transition: border-color 0.2s, box-shadow 0.2s;
}
.waypoint-input:focus {
    outline: none; border-color: var(--brand-secondary);
    box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
}
.remove-waypoint {
    position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
    background: #fee2e2; color: var(--error-color); border: none; border-radius: 50%;
    width: 24px; height: 24px; cursor: pointer; line-height: 1;
    transition: background-color 0.2s, color 0.2s;
    display: none; align-items: center; justify-content: center;
}
.remove-waypoint .material-icons { font-size: 20px; }
.remove-waypoint:hover { background: var(--error-color); color: white; }
.input-group[draggable="true"] .remove-waypoint { display: flex; }

#add-waypoint-btn {
    background: #f9fafb; border: 1px dashed #d1d5db; color: #4b5563;
    width: 100%; padding: 10px; border-radius: 6px; cursor: pointer;
    font-weight: 500; margin-bottom: 15px; transition: all 0.2s;
    display: flex; align-items: center; justify-content: center; gap: 8px;
}
#add-waypoint-btn:hover { background: #f3f4f6; border-color: #9ca3af; }

.options {
    display: flex; align-items: center; background-color: #f9fafb;
    border: 1px solid var(--border-color); border-radius: 8px;
    padding: 12px; margin-bottom: 15px; cursor: pointer;
    transition: background-color 0.2s;
}
.options:hover { background-color: #f3f4f6; }
.options input[type="checkbox"] {
    margin-right: 12px; width: 16px; height: 16px;
    accent-color: var(--brand-primary); cursor: pointer;
}
.options label { font-weight: 500; color: var(--text-secondary); cursor: pointer; flex-grow: 1; }

.transport-modes { display: flex; align-items: center; margin-bottom: 15px; border: 1px solid #d1d5db; border-radius: 6px; overflow: hidden; }
.transport-modes button {
    flex-grow: 1; background: white; border: none; padding: 10px;
    cursor: pointer; font-weight: 500; color: #4b5563;
    transition: all 0.2s; border-right: 1px solid #d1d5db;
    display: flex; align-items: center; justify-content: center; gap: 4px;
}
.transport-modes button:last-child { border-right: none; }
.transport-modes button.active { background: var(--brand-primary); color: white; }
.transport-modes button:hover:not(.active) { background-color: #f3f4f6; }

.action-button {
    background-color: var(--brand-primary); color: white; border: none; padding: 12px 15px;
    border-radius: 8px; font-weight: 500; cursor: pointer; width: 100%;
    font-size: 1em; transition: all 0.2s ease-in-out;
    display: flex; align-items: center; justify-content: center; gap: 8px;
}
.action-button:hover:not(:disabled) { background-color: #112546; transform: translateY(-2px); }
.action-button:disabled { background-color: #9ca3af; cursor: not-allowed; transform: none; }
.action-button .loader { border-top-color: white; }

.secondary-button { background-color: var(--brand-secondary); margin-top: 10px; }
.secondary-button:hover:not(:disabled) { background-color: #2980b9; }
.tertiary-button { background-color: var(--brand-accent); margin-top: 10px; }
.tertiary-button:hover:not(:disabled) { background-color: #d97706; }

.custom-marker { display: flex; justify-content: center; align-items: center; width: 28px; height: 28px; border-radius: 50%; background: var(--brand-primary); color: white; font-weight: bold; font-size: 14px; border: 2px solid white; box-shadow: 0 2px 5px rgba(0,0,0,0.3); }
.driver-marker { background: var(--success-color); }
.urgent-marker { background: var(--brand-accent); animation: pulse 1.s infinite; }
@keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(246, 184, 12, 0.7); } 70% { box-shadow: 0 0 0 15px rgba(246, 184, 12, 0); } 100% { box-shadow: 0 0 0 0 rgba(246, 184, 12, 0); } }

#reset-view-btn {
    position: absolute; top: 20px; right: 20px; z-index: 5;
    background-color: var(--surface-panel); color: var(--brand-primary); border: 1px solid var(--border-color);
    padding: 10px 15px; border-radius: 8px; font-weight: 500; cursor: pointer;
    box-shadow: 0 4px 15px rgba(0,0,0,0.1); transition: all 0.2s;
    display: none; align-items: center; gap: 8px;
}
#reset-view-btn:hover { background-color: #f3f4f6; }

.truck-marker {
    width: 40px; height: 40px;
    background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="%2310b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-truck"><path d="M5 18H3c-.6 0-1-.4-1-1V7c0-.6.4-1 1-1h10c.6 0 1 .4 1 1v11"/><path d="M14 9h4l4 4v4c0 .6-.4 1-1 1h-2"/><circle cx="7" cy="18" r="2"/><path d="M9 18h6"/><circle cx="18" cy="18" r="2"/></svg>');
    background-size: 75%; background-repeat: no-repeat; background-position: center;
    background-color: rgba(255,255,255,0.8); border-radius: 50%;
    border: 2px solid var(--success-color);
    box-shadow: 0 0 10px rgba(16, 185, 129, 0.7);
}

/* =================================================================== */
/* RESPONSIVE & DESKTOP STYLES                                         */
/* =================================================================== */

/* --- Mobile view (up to 768px) --- */
@media (max-width: 768px) {
    body {
        height: 100vh;
        margin: 0;
        overflow: hidden; /* Prevents the entire page from scrolling */
    }

    .control-panel {
        position: fixed; /* Positions the panel over the map */
        bottom: 0;
        left: 0;
        right: 0;
        width: 100%;
        box-sizing: border-box;
        max-height: 60vh; /* Takes up at most 60% of the viewport height */
        overflow-y: auto; /* Becomes scrollable when content exceeds height */
        z-index: 10;

        /* Glassmorphism effect */
        background: rgba(255, 255, 255, 0.85);
        -webkit-backdrop-filter: blur(10px);
        backdrop-filter: blur(10px);

        box-shadow: 0 -4px 15px rgba(0,0,0,0.1);
        border-top: 1px solid rgba(255, 255, 255, 0.2);
        border-radius: 20px 20px 0 0; /* Rounded top corners */
        padding: 8px 16px 16px 16px;

        transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
        transform: translateY(0); /* Panel is visible by default */
    }

    h1 {
        font-size: 1.5rem;
        margin-top: 8px;
        margin-bottom: 12px;
        text-align: center;
    }

    #reset-view-btn {
        top: 10px;
        right: 10px;
        z-index: 11; /* Ensures the button is above the panel */
    }
}

/* --- Desktop view (from 769px) --- */
@media (min-width: 769px) {
    .control-panel {
        position: absolute;
        top: 20px;
        left: 20px;
        z-index: 10;
        background-color: var(--surface-panel, white);
        border-radius: 12px;
        box-shadow: 0 8px 30px rgba(0,0,0,0.12);
        width: 420px;
        max-height: calc(100vh - 40px);
        overflow-y: auto;
        padding: 10px 24px 24px 24px;
    }
}
<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8" />
    <title>SmartMaps Showcase: Logistik & Touren-Optimierer</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no" />

    <!-- External stylesheets (only needed for Autocomplete) -->
    <link rel="stylesheet" href="https://cdn.smartmaps.cloud/packages/smartmaps/autocomplete/autocomplete.css" />
    <link href="css/material-icons.css" rel="stylesheet">

    <!-- SmartMaps libraries -->
    <script src="https://cdn.smartmaps.cloud/packages/smartmaps/autocomplete/v5/umd/autocomplete.min.js"></script>
    <script src="https://cdn.smartmaps.cloud/packages/smartmaps/smartmaps-gl/v2/umd/smartmaps-gl.min.js"></script>

    <style>
        /* =================================================================== */
        /* BASE STYLES (integrated from smartmaps-demo-styles.css)             */
        /* =================================================================== */

        :root {
            --brand-primary: #18345c;
            --brand-secondary: #3498db;
            --brand-accent: #f6b80c;
            --text-primary: #111827;
            --text-secondary: #374151;
            --text-light: #6b7280;
            --surface-bg: #f4f7fa;
            --surface-panel: #ffffff;
            --border-color: #e5e7eb;
            --success-color: #10b981;
            --error-color: #ef4444;
        }

        body {
            margin: 0;
            padding: 0;
            font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            background-color: var(--surface-bg);
            color: var(--text-primary);
            -webkit-font-smoothing: antialiased;
            -moz-osx-font-smoothing: grayscale;
        }

        #map {
            position: absolute;
            top: 0;
            bottom: 0;
            width: 100%;
            height: 100%;
        }

        .loader {
            border: 4px solid rgba(0,0,0,0.1);
            border-top: 4px solid var(--brand-secondary);
            border-radius: 50%;
            width: 20px;
            height: 20px;
            animation: spin 1s linear infinite;
            margin: 0 auto;
        }
        @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }

        .info-icon {
            position: relative;
            display: inline-flex;
            align-items: center;
            cursor: pointer;
            color: #9ca3af;
        }
        .info-icon .tooltip {
            visibility: hidden; width: 260px; background-color: #1f2937;
            color: #fff; text-align: left; border-radius: 6px; padding: 12px;
            position: absolute; z-index: 100; opacity: 0; transition: opacity 0.3s;
            font-size: 0.85rem; line-height: 1.5; font-weight: 400;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15); bottom: 130%; right: 0;
            left: auto; transform: translateX(0);
        }
        .info-icon .tooltip::after {
            content: ""; position: absolute; top: 100%; right: 10px; left: auto;
            margin-left: 0; border-width: 5px; border-style: solid;
            border-color: #1f2937 transparent transparent transparent;
        }
        .info-icon:hover .tooltip { visibility: visible; opacity: 1; }
        .info-icon .tooltip strong { color: var(--brand-secondary); }

        /* =================================================================== */
        /* DEMO-SPECIFIC STYLES                                                */
        /* =================================================================== */
        h1 {
            background: var(--brand-primary);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            font-size: 1.75rem;
        }

        .control-panel { display: flex; flex-direction: column; }
        .accordion-item { border-bottom: 1px solid var(--border-color); }
        .accordion-item:last-child { border-bottom: none; }
        .accordion-header {
            background: none; border: none; width: 100%; text-align: left;
            padding: 18px 4px; cursor: pointer; display: flex;
            justify-content: space-between; align-items: center;
            transition: background-color 0.2s;
        }
        .accordion-header:hover { background-color: #f9fafb; }
        .accordion-header h2 {
            font-size: 1.1em; font-weight: 500; margin: 0;
            color: var(--text-secondary); border: none; padding: 0;
            display: flex; align-items: center; gap: 8px;
        }
        .accordion-header .info-icon { margin-left: auto; margin-right: 10px; }
        .accordion-header .chevron { transition: transform 0.3s ease; }
        .accordion-header.active .chevron { transform: rotate(180deg); }
        .accordion-header[disabled] { cursor: not-allowed; background-color: #f9fafb; }
        .accordion-header[disabled] h2 { color: #9ca3af; }
        .accordion-body { max-height: 0; overflow: hidden; transition: max-height 0.4s ease-out; }
        .accordion-content { padding: 1px 0 18px 0; }

        .input-group {
            margin-bottom: 12px; position: relative; padding-left: 10px;
            border-left: 4px solid transparent;
        }
        .input-group[draggable="true"] { cursor: move; border-left: 4px solid #d1d5db; }
        .input-group.dragging { opacity: 0.4; background: #eef2f7; }
        .waypoint-input {
            width: 100%; padding: 10px; box-sizing: border-box; font-size: 1em;
            border: 1px solid #d1d5db; border-radius: 6px;
            transition: border-color 0.2s, box-shadow 0.2s;
        }
        .waypoint-input:focus {
            outline: none; border-color: var(--brand-secondary);
            box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
        }
        .remove-waypoint {
            position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
            background: #fee2e2; color: var(--error-color); border: none; border-radius: 50%;
            width: 24px; height: 24px; cursor: pointer; line-height: 1;
            transition: background-color 0.2s, color 0.2s;
            display: none; align-items: center; justify-content: center;
        }
        .remove-waypoint .material-icons { font-size: 20px; }
        .remove-waypoint:hover { background: var(--error-color); color: white; }
        .input-group[draggable="true"] .remove-waypoint { display: flex; }

        #add-waypoint-btn {
            background: #f9fafb; border: 1px dashed #d1d5db; color: #4b5563;
            width: 100%; padding: 10px; border-radius: 6px; cursor: pointer;
            font-weight: 500; margin-bottom: 15px; transition: all 0.2s;
            display: flex; align-items: center; justify-content: center; gap: 8px;
        }
        #add-waypoint-btn:hover { background: #f3f4f6; border-color: #9ca3af; }

        .options {
            display: flex; align-items: center; background-color: #f9fafb;
            border: 1px solid var(--border-color); border-radius: 8px;
            padding: 12px; margin-bottom: 15px; cursor: pointer;
            transition: background-color 0.2s;
        }
        .options:hover { background-color: #f3f4f6; }
        .options input[type="checkbox"] {
            margin-right: 12px; width: 16px; height: 16px;
            accent-color: var(--brand-primary); cursor: pointer;
        }
        .options label { font-weight: 500; color: var(--text-secondary); cursor: pointer; flex-grow: 1; }

        .transport-modes { display: flex; align-items: center; margin-bottom: 15px; border: 1px solid #d1d5db; border-radius: 6px; overflow: hidden; }
        .transport-modes button {
            flex-grow: 1; background: white; border: none; padding: 10px;
            cursor: pointer; font-weight: 500; color: #4b5563;
            transition: all 0.2s; border-right: 1px solid #d1d5db;
            display: flex; align-items: center; justify-content: center; gap: 4px;
        }
        .transport-modes button:last-child { border-right: none; }
        .transport-modes button.active { background: var(--brand-primary); color: white; }
        .transport-modes button:hover:not(.active) { background-color: #f3f4f6; }

        .action-button {
            background-color: var(--brand-primary); color: white; border: none; padding: 12px 15px;
            border-radius: 8px; font-weight: 500; cursor: pointer; width: 100%;
            font-size: 1em; transition: all 0.2s ease-in-out;
            display: flex; align-items: center; justify-content: center; gap: 8px;
        }
        .action-button:hover:not(:disabled) { background-color: #112546; transform: translateY(-2px); }
        .action-button:disabled { background-color: #9ca3af; cursor: not-allowed; transform: none; }
        .action-button .loader { border-top-color: white; }

        .secondary-button { background-color: var(--brand-secondary); margin-top: 10px; }
        .secondary-button:hover:not(:disabled) { background-color: #2980b9; }
        .tertiary-button { background-color: var(--brand-accent); margin-top: 10px; }
        .tertiary-button:hover:not(:disabled) { background-color: #d97706; }

        .custom-marker { display: flex; justify-content: center; align-items: center; width: 28px; height: 28px; border-radius: 50%; background: var(--brand-primary); color: white; font-weight: bold; font-size: 14px; border: 2px solid white; box-shadow: 0 2px 5px rgba(0,0,0,0.3); }
        .driver-marker { background: var(--success-color); }
        .urgent-marker { background: var(--brand-accent); animation: pulse 1.s infinite; }
        @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(246, 184, 12, 0.7); } 70% { box-shadow: 0 0 0 15px rgba(246, 184, 12, 0); } 100% { box-shadow: 0 0 0 0 rgba(246, 184, 12, 0); } }

        #reset-view-btn {
            position: absolute; top: 20px; right: 20px; z-index: 5;
            background-color: var(--surface-panel); color: var(--brand-primary); border: 1px solid var(--border-color);
            padding: 10px 15px; border-radius: 8px; font-weight: 500; cursor: pointer;
            box-shadow: 0 4px 15px rgba(0,0,0,0.1); transition: all 0.2s;
            display: none; align-items: center; gap: 8px;
        }
        #reset-view-btn:hover { background-color: #f3f4f6; }

        .truck-marker {
            width: 40px; height: 40px;
            background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="%2310b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-truck"><path d="M5 18H3c-.6 0-1-.4-1-1V7c0-.6.4-1 1-1h10c.6 0 1 .4 1 1v11"/><path d="M14 9h4l4 4v4c0 .6-.4 1-1 1h-2"/><circle cx="7" cy="18" r="2"/><path d="M9 18h6"/><circle cx="18" cy="18" r="2"/></svg>');
            background-size: 75%; background-repeat: no-repeat; background-position: center;
            background-color: rgba(255,255,255,0.8); border-radius: 50%;
            border: 2px solid var(--success-color);
            box-shadow: 0 0 10px rgba(16, 185, 129, 0.7);
        }

        /* =================================================================== */
        /* RESPONSIVE & DESKTOP STYLES                                         */
        /* =================================================================== */

        /* --- Mobile view (up to 768px) --- */
        @media (max-width: 768px) {
            body {
                height: 100vh;
                margin: 0;
                overflow: hidden; /* Prevents the entire page from scrolling */
            }

            .control-panel {
                position: fixed; /* Positions the panel over the map */
                bottom: 0;
                left: 0;
                right: 0;
                width: 100%;
                box-sizing: border-box;
                max-height: 60vh; /* Takes up at most 60% of the viewport height */
                overflow-y: auto; /* Becomes scrollable when content exceeds height */
                z-index: 10;

                /* Glassmorphism effect */
                background: rgba(255, 255, 255, 0.85);
                -webkit-backdrop-filter: blur(10px);
                backdrop-filter: blur(10px);

                box-shadow: 0 -4px 15px rgba(0,0,0,0.1);
                border-top: 1px solid rgba(255, 255, 255, 0.2);
                border-radius: 20px 20px 0 0; /* Rounded top corners */
                padding: 8px 16px 16px 16px;

                transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
                transform: translateY(0); /* Panel is visible by default */
            }

            h1 {
                font-size: 1.5rem;
                margin-top: 8px;
                margin-bottom: 12px;
                text-align: center;
            }

            #reset-view-btn {
                top: 10px;
                right: 10px;
                z-index: 11; /* Ensures the button is above the panel */
            }
        }

        /* --- Desktop view (from 769px) --- */
        @media (min-width: 769px) {
            .control-panel {
                position: absolute;
                top: 20px;
                left: 20px;
                z-index: 10;
                background-color: var(--surface-panel, white);
                border-radius: 12px;
                box-shadow: 0 8px 30px rgba(0,0,0,0.12);
                width: 420px;
                max-height: calc(100vh - 40px);
                overflow-y: auto;
                padding: 10px 24px 24px 24px;
            }
        }
    </style>
</head>
<body>

    <div id="map"></div>
    <button id="reset-view-btn"><i class="material-icons">arrow_back</i> Zurück zur Übersicht</button>
    <div class="control-panel" id="control-panel">
        <h1>Logistik & Touren-Optimierer</h1>

        <div id="accordion-container">
            <div class="accordion-item">
                <button class="accordion-header" id="header-planner">
                    <h2>1. Tour planen</h2>
                    <span class="info-icon">
                        <i class="material-icons">info</i>
                        <span class="tooltip">Nutzt die <strong>Autocomplete API</strong> und <strong>Routing API (Trip/Route)</strong>.</span>
                    </span>
                    <i class="material-icons chevron">expand_more</i>
                </button>
                <div class="accordion-body" id="body-planner">
                    <div class="accordion-content">
                        <div id="waypoints-container">
                            <div class="input-group"><input class="waypoint-input" type="search" placeholder="Start / Depot" /></div>
                            <div class="input-group"><input class="waypoint-input" type="search" placeholder="Zieladresse" /></div>
                        </div>
                        <button id="add-waypoint-btn"><i class="material-icons">add</i> Zwischenstopp</button>
                        <div class="options">
                            <input type="checkbox" id="optimize-checkbox">
                            <label for="optimize-checkbox">Beste Reihenfolge finden</label>
                        </div>
                        <div class="transport-modes">
                            <button class="active" data-profile="FAST"><i class="material-icons">directions_car</i><span>PKW</span></button>
                            <button data-profile="BICYCLE"><i class="material-icons">directions_bike</i><span>Rad</span></button>
                            <button data-profile="PEDESTRIAN"><i class="material-icons">directions_walk</i><span>Fuß</span></button>
                        </div>
                        <button id="calculate-btn" class="action-button" disabled><i class="material-icons">route</i> Route berechnen</button>
                    </div>
                </div>
            </div>

            <div class="accordion-item">
                <button class="accordion-header" id="header-results" disabled>
                    <h2>2. Ergebnis & Analyse</h2>
                    <span class="info-icon">
                        <i class="material-icons">info</i>
                        <span class="tooltip">Nutzt die <strong>Routing API (Isochrone)</strong>.</span>
                    </span>
                    <i class="material-icons chevron">expand_more</i>
                </button>
                <div class="accordion-body" id="body-results">
                    <div class="accordion-content">
                        <div id="route-summary"></div>
                        <button id="isochrone-btn" class="action-button secondary-button"><i class="material-icons">map</i>15-Min Servicegebiet</button>
                    </div>
                </div>
            </div>

            <div class="accordion-item">
                <button class="accordion-header" id="header-simulation" disabled>
                    <h2>3. Notfall-Simulation</h2>
                    <span class="info-icon">
                        <i class="material-icons">info</i>
                        <span class="tooltip">Nutzt die <strong>Routing API (Matrix)</strong>.</span>
                    </span>
                    <i class="material-icons chevron">expand_more</i>
                </button>
                <div class="accordion-body" id="body-simulation">
                    <div class="accordion-content">
                        <p>Ein Eilauftrag kommt rein. Welcher Fahrer ist am schnellsten da?</p>
                        <button id="matrix-btn" class="action-button secondary-button"><i class="material-icons">groups</i> Nächsten Fahrer finden</button>
                    </div>
                </div>
            </div>

            <div class="accordion-item">
                <button class="accordion-header" id="header-tracking" disabled>
                    <h2>4. Fahrer-Tracking</h2>
                    <span class="info-icon">
                        <i class="material-icons">info</i>
                        <span class="tooltip">Nutzt die <strong>Map Match API</strong> (simuliert durch Routing).</span>
                    </span>
                    <i class="material-icons chevron">expand_more</i>
                </button>
                <div class="accordion-body" id="body-tracking">
                    <div class="accordion-content" id="matrix-result"></div>
                </div>
            </div>
        </div>
    </div>

    <script>
        /*
         * =====================================================================
         * SmartMaps Logistics & Tour Optimizer
         * =====================================================================
         *
         * This demo showcases a 4-step logistics workflow using the SmartMaps
         * Routing API family:
         *
         *   1. TOUR PLANNING
         *      Users enter addresses via the Autocomplete API, optionally add
         *      intermediate stops with drag-and-drop reordering, and choose a
         *      transport mode. The route is calculated using the Routing API
         *      (type ROUTE) or the Trip optimizer (type TRIP) when "find best
         *      order" is checked.
         *
         *   2. RESULTS & ANALYSIS
         *      The computed route is displayed on the map with distance/duration
         *      summary. An isochrone analysis (type ISOCHRONE) visualizes the
         *      15-minute reachable service area around the last waypoint.
         *
         *   3. EMERGENCY SIMULATION
         *      A simulated urgent delivery appears near the destination. Using
         *      the Matrix API (type MATRIX), the system identifies which of the
         *      randomly placed drivers can reach the pickup fastest.
         *
         *   4. DRIVER TRACKING
         *      The nearest driver's route is calculated and an animated truck
         *      marker follows the route with smooth camera tracking (zoom, pitch,
         *      and center interpolation).
         *
         * API endpoints used:
         *   - Routing API: https://www.yellowmap.de/api_rst/v2/geojson/route
         *   - Autocomplete: SmartMaps Autocomplete JS SDK (v5)
         * =====================================================================
         */

        // === CONFIGURATION & CONSTANTS =======================================

        /** SmartMaps API key for authentication */
        const apiKey = '[INSERT API-KEY]';

        /** Isochrone reachability time in minutes */
        const ISOCHRONE_TIME_MINUTES = 15;

        /** Isochrone grid resolution (higher = more detailed polygon) */
        const ISOCHRONE_GRID = "100";

        /** Number of simulated drivers placed around the destination */
        const DRIVER_COUNT = 10;

        /** Radius in km within which drivers are randomly distributed */
        const DRIVER_SPREAD_RADIUS_KM = 5;

        /** Lat/lng offset for the urgent delivery marker from the last waypoint */
        const URGENT_DELIVERY_OFFSET = 0.01;

        /** Duration of the driver tracking animation in milliseconds */
        const TRACKING_ANIMATION_DURATION_MS = 25000;

        /** Target zoom level during driver tracking animation */
        const TRACKING_TARGET_ZOOM = 15.5;

        /** Target pitch (tilt) during driver tracking animation */
        const TRACKING_TARGET_PITCH = 55;

        /** Smoothing factor for camera interpolation (0-1, higher = smoother) */
        const TRACKING_SMOOTHING = 0.98;

        /** Approximate km per degree of latitude (1 degree latitude ~ 111.32 km) */
        const KM_TO_DEGREES = 111.32;


        // === MAP INITIALIZATION ==============================================

        /**
         * Reads a CSS custom property from the document root.
         * @param {string} variable - CSS variable name (e.g. '--brand-primary')
         * @returns {string} The computed value of the CSS variable
         */
        function getCssVariable(variable) {
            return getComputedStyle(document.documentElement).getPropertyValue(variable).trim();
        }

        const map = new smartmapsgl.Map({
            apiKey: apiKey,
            container: 'map',
            center: { lat: 49.021649, lng: 8.439330 },
            zoom: 6,
            style: smartmapsgl.MapStyle.ESSENTIAL
        });


        // === DOM ELEMENTS & STATE ============================================

        const controlPanel = document.getElementById('control-panel');
        const waypointsContainer = document.getElementById('waypoints-container');
        const addWaypointBtn = document.getElementById('add-waypoint-btn');
        const calculateBtn = document.getElementById('calculate-btn');
        const isochroneBtn = document.getElementById('isochrone-btn');
        const matrixBtn = document.getElementById('matrix-btn');
        const optimizeCheckbox = document.getElementById('optimize-checkbox');
        const transportButtons = document.querySelectorAll('.transport-modes button');
        const resetViewBtn = document.getElementById('reset-view-btn');

        /** @type {Array<{input: HTMLInputElement, coords: number[]|null, group: HTMLElement}>} */
        let waypoints = [];
        /** @type {smartmapsgl.Marker[]} */
        let mapMarkers = [];
        /** Currently selected routing speed profile */
        let currentSpeedProfile = 'FAST';
        /** Coordinates of simulated driver locations */
        let dynamicDriverLocations = [];
        /** Coordinates of the urgent delivery marker */
        let urgentDeliveryLocation = null;
        /** Info about the best (fastest) driver from the matrix result */
        let bestDriverInfo = {};

        /** requestAnimationFrame ID for the tracking animation */
        let animationFrameId;
        /** GeoJSON feature of the driver-to-delivery route */
        let driverRouteGeoJSON = null;
        /** The animated truck marker instance */
        let truckMarker = null;
        /** Timestamp when the tracking animation started */
        let animationStartTime;


        // === WAYPOINT MANAGEMENT =============================================

        /**
         * Initializes autocomplete on a waypoint input field and registers
         * it in the waypoints array.
         * @param {HTMLInputElement} inputElement - The input element to attach autocomplete to
         */
        async function initializeWaypointInput(inputElement) {
            const waypoint = { input: inputElement, coords: null, group: inputElement.parentElement };
            waypoints.push(waypoint);

            const autocomplete = await smartmaps.autocompleteService.createAutocomplete(inputElement, apiKey, {});
            autocomplete.addEventListener('selected', (e) => {
                waypoint.coords = e.detail.geojson.geometry.coordinates;
                checkCalculable();
            });
            inputElement.addEventListener('input', () => { waypoint.coords = null; checkCalculable(); });
        }

        /**
         * Adds a new intermediate waypoint input field between the first and
         * last stops. Attaches autocomplete, a remove button, and re-indexes
         * all placeholders.
         */
        async function addWaypoint() {
            const newGroup = document.createElement('div');
            newGroup.className = 'input-group';
            newGroup.innerHTML = `<input class="waypoint-input" type="search" placeholder="Zwischenstopp" /><button class="remove-waypoint"><i class="material-icons">close</i></button>`;

            const lastInputGroup = waypointsContainer.querySelector('.input-group:last-of-type');
            waypointsContainer.insertBefore(newGroup, lastInputGroup);

            const newInput = newGroup.querySelector('input');
            const newIndex = waypoints.length - 1;
            const newWaypoint = { input: newInput, coords: null, group: newGroup };
            waypoints.splice(newIndex, 0, newWaypoint);

            const autocomplete = await smartmaps.autocompleteService.createAutocomplete(newInput, apiKey, {});
            autocomplete.addEventListener('selected', (e) => {
                newWaypoint.coords = e.detail.geojson.geometry.coordinates;
                checkCalculable();
            });
            newInput.addEventListener('input', () => { newWaypoint.coords = null; checkCalculable(); });

            newGroup.querySelector('.remove-waypoint').addEventListener('click', () => {
                waypoints = waypoints.filter(wp => wp !== newWaypoint);
                newGroup.remove();
                checkCalculable();
                updateWaypointPlaceholders();
                const activeHeader = document.querySelector('.accordion-header.active');
                if (activeHeader) {
                    const body = activeHeader.nextElementSibling;
                    body.style.maxHeight = body.scrollHeight + "px";
                }
            });

            updateWaypointPlaceholders();
            const plannerHeader = document.getElementById('header-planner');
            if(plannerHeader.classList.contains('active')) {
                const body = plannerHeader.nextElementSibling;
                body.style.maxHeight = body.scrollHeight + "px";
            }
        }

        /**
         * Enables or disables the "Calculate" button depending on whether
         * all waypoints have resolved coordinates.
         */
        function checkCalculable() {
            calculateBtn.disabled = !waypoints.every(wp => wp.coords !== null);
        }

        /**
         * Updates placeholder text and draggable state for all waypoint input
         * groups. The first is always "Start / Depot", the last is always
         * "Zieladresse", and intermediates are numbered "Zwischenstopp N".
         */
        function updateWaypointPlaceholders() {
            const inputGroups = waypointsContainer.querySelectorAll('.input-group');
            inputGroups.forEach((group, index) => {
                const input = group.querySelector('input');
                if (index === 0) {
                    input.placeholder = 'Start / Depot';
                    group.draggable = false;
                } else if (index === inputGroups.length - 1) {
                    input.placeholder = 'Zieladresse';
                    group.draggable = false;
                } else {
                    input.placeholder = `Zwischenstopp ${index}`;
                    group.draggable = true;
                }
            });
        }


        // === ROUTE CALCULATION ===============================================

        /**
         * Builds a standard GeoJSON FeatureCollection request body for the
         * SmartMaps Routing API.
         * @param {string} type - Routing type: "ROUTE", "TRIP", "ISOCHRONE", or "MATRIX"
         * @param {Object} extraParams - Additional routing parameters to merge
         * @param {Array} features - GeoJSON Feature array for the request
         * @returns {Object} Complete request body ready for JSON.stringify
         */
        function buildRoutingRequest(type, extraParams, features) {
            return {
                type: "FeatureCollection",
                routingparams: { type, ...extraParams },
                authentication: { channel: "roadtrip-demo" },
                crs: { type: "name", properties: { name: "urn:ogc:def:crs:OGC:1.3:CRS84" } },
                features
            };
        }

        /**
         * Returns responsive map padding that accounts for the control panel
         * position on mobile (bottom sheet) vs. desktop (left sidebar).
         * @returns {{top: number, bottom: number, left: number, right: number}} Padding in pixels
         */
        function getMapPadding() {
            if (window.innerWidth <= 768) {
                const panel = document.getElementById('control-panel');
                // Panel height + 20px extra spacing
                const panelHeight = panel ? panel.offsetHeight : 300;
                return { top: 40, bottom: panelHeight + 20, left: 20, right: 20 };
            } else {
                // Desktop padding: accounts for the left-side panel
                return { top: 40, bottom: 40, left: 460, right: 40 };
            }
        }

        /**
         * Calculates a route or optimized trip from the entered waypoints.
         * Uses TRIP mode when the "find best order" checkbox is checked,
         * otherwise uses standard ROUTE mode. Draws the result on the map
         * and displays distance/duration in the results accordion.
         */
        async function calculateRoute() {
            calculateBtn.disabled = true;
            calculateBtn.innerHTML = '<span>Berechne...</span><div class="loader"></div>';
            clearMap();

            const coords = waypoints.map(wp => wp.coords);
            const useTrip = optimizeCheckbox.checked;

            const url = `https://www.yellowmap.de/api_rst/v2/geojson/route?apiKey=${smartmapsgl.encodeString(apiKey)}`;
            const requestBody = buildRoutingRequest(
                useTrip ? "TRIP" : "ROUTE",
                { speedProfile: currentSpeedProfile, routingRoundTrip: false, isoLocale: "de-DE", coordFormatOut: "GEODECIMAL_POINT" },
                useTrip
                    ? [{ type: "Feature", geometry: { type: "MultiPoint", coordinates: coords }, properties: {} }]
                    : coords.map(c => ({ type: "Feature", geometry: { type: "Point", coordinates: c }, properties: {} }))
            );

            try {
                const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) });
                if (!response.ok) throw new Error(`API Error: ${response.status} - ${await response.text()}`);
                const data = await response.json();

                if (data.features && data.features.length > 0) {
                    const routeFeature = data.features[0];
                    const props = data.properties;
                    document.getElementById('route-summary').innerHTML = `<strong>Distanz:</strong> ${(props.distance / 1000).toFixed(1)} km<br><strong>Dauer:</strong> ca. ${new Date(props.duration * 1000).toISOString().substr(11, 8)}`;

                    map.addSource('route', { type: 'geojson', data: routeFeature });
                    map.addLayer({ id: 'route-line', type: 'line', source: 'route', layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-color': getCssVariable('--brand-primary'), 'line-width': 6, 'line-opacity': 0.8 } });

                    // When using TRIP mode, read back the optimized waypoint order
                    let stops = coords;
                    if (useTrip && routeFeature.properties && routeFeature.properties.waypoints) {
                        stops = routeFeature.properties.waypoints.map(wp => wp.originalRequest.geometry.coordinates);
                    }

                    // Place numbered markers at each stop
                    stops.forEach((coord, index) => {
                        const el = document.createElement('div');
                        el.className = 'custom-marker';
                        el.textContent = index === 0 ? 'A' : (index === stops.length - 1 ? 'B' : index);
                        mapMarkers.push(new smartmapsgl.Marker({ element: el }).setLngLat(coord).addTo(map));
                    });

                    // Fit the map to the route with responsive padding
                    const bounds = new smartmapsgl.LngLatBounds();
                    routeFeature.geometry.coordinates.forEach(coord => bounds.extend(coord));
                    map.fitBounds(bounds, { padding: getMapPadding() });

                    toggleAccordion(document.getElementById('header-results'), true);

                } else { throw new Error("No route found in the API response."); }
            } catch (error) {
                console.error("Route calculation error:", error);
                document.getElementById('route-summary').innerHTML = `<p style="color: var(--error-color);">Route konnte nicht berechnet werden.</p>`;
                toggleAccordion(document.getElementById('header-results'), true);
            } finally {
                calculateBtn.disabled = false;
                calculateBtn.innerHTML = '<i class="material-icons">route</i> Route berechnen';
            }
        }


        // === ISOCHRONE ANALYSIS ==============================================

        /**
         * Calculates and displays an isochrone polygon around the last waypoint.
         * The isochrone shows the area reachable within ISOCHRONE_TIME_MINUTES
         * using the currently selected speed profile.
         */
        isochroneBtn.addEventListener('click', async () => {
            const startCoords = waypoints[waypoints.length - 1].coords;
            if (!startCoords) return;

            isochroneBtn.disabled = true;
            isochroneBtn.innerHTML = '<span>Lade...</span><div class="loader"></div>';

            const url = `https://www.yellowmap.de/api_rst/v2/geojson/route?apiKey=${smartmapsgl.encodeString(apiKey)}`;
            const body = buildRoutingRequest(
                "ISOCHRONE",
                { timeInMinutes: ISOCHRONE_TIME_MINUTES, speedProfile: currentSpeedProfile, isochroneGrid: ISOCHRONE_GRID, coordFormatOut: "GEODECIMAL_POINT" },
                [{ type: "Feature", geometry: { type: "Point", coordinates: startCoords }, properties: {} }]
            );

            try {
                const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
                if (!response.ok) throw new Error(`API Error: ${response.status}`);
                const data = await response.json();

                if (data.features && data.features.length > 0) {
                    const isochroneFeature = data.features[0];
                    // Remove any existing isochrone layers before adding new ones
                    if (map.getSource('isochrone')) { map.removeLayer('isochrone-outline'); map.removeLayer('isochrone-area'); map.removeSource('isochrone'); }
                    map.addSource('isochrone', { type: 'geojson', data: isochroneFeature });

                    // Insert isochrone layers below the route line so the route stays visible
                    const beforeId = map.getLayer('route-line') ? 'route-line' : undefined;
                    map.addLayer({ id: 'isochrone-area', type: 'fill', source: 'isochrone', paint: { 'fill-color': getCssVariable('--brand-secondary'), 'fill-opacity': 0.3 } }, beforeId);
                    map.addLayer({ id: 'isochrone-outline', type: 'line', source: 'isochrone', paint: { 'line-color': '#ffffff', 'line-width': 2, 'line-opacity': 0.9 } }, beforeId);

                    // Fit the map to the isochrone bounds with responsive padding
                    const bounds = new smartmapsgl.LngLatBounds();
                    isochroneFeature.geometry.coordinates[0].forEach(coord => bounds.extend(coord));
                    map.fitBounds(bounds, { padding: getMapPadding(), duration: 1000 });

                    toggleAccordion(document.getElementById('header-simulation'), true);
                } else { console.error("Isochrone response contains no features."); }
            } catch (error) { console.error("Isochrone calculation error:", error);
            } finally {
                isochroneBtn.disabled = false;
                isochroneBtn.innerHTML = '<i class="material-icons">map</i> 15-Min Servicegebiet';
            }
        });


        // === MATRIX / EMERGENCY DISPATCH =====================================

        /**
         * Simulates an emergency dispatch scenario. Places random driver markers
         * and an urgent delivery marker on the map, then uses the Matrix API to
         * find the fastest driver. Results are displayed in the tracking accordion.
         */
        matrixBtn.addEventListener('click', async () => {
            const destination = waypoints[waypoints.length - 1].coords;
            if (!destination) return;

            // Remove any existing driver/urgent markers
            mapMarkers.filter(m => m.getElement().classList.contains('driver-marker') || m.getElement().classList.contains('urgent-marker')).forEach(m => m.remove());
            mapMarkers = mapMarkers.filter(m => !m.getElement().classList.contains('driver-marker') && !m.getElement().classList.contains('urgent-marker'));

            // Generate random driver positions and the urgent delivery location
            dynamicDriverLocations = generateDriverLocations(destination, DRIVER_COUNT, DRIVER_SPREAD_RADIUS_KM);
            urgentDeliveryLocation = [destination[0] + URGENT_DELIVERY_OFFSET, destination[1] + URGENT_DELIVERY_OFFSET];

            // Add driver markers to the map
            dynamicDriverLocations.forEach((coords, i) => {
                const el = document.createElement('div');
                el.className = 'custom-marker driver-marker';
                el.textContent = `F${i+1}`;
                mapMarkers.push(new smartmapsgl.Marker({ element: el }).setLngLat(coords).addTo(map));
            });

            // Add urgent delivery marker with pulsing animation
            const el = document.createElement('div');
            el.className = 'custom-marker urgent-marker';
            el.textContent = `!`;
            mapMarkers.push(new smartmapsgl.Marker({ element: el }).setLngLat(urgentDeliveryLocation).addTo(map));

            // Call the Matrix API to find travel times from the urgent location to all drivers
            const url = `https://www.yellowmap.de/api_rst/v2/geojson/route?apiKey=${smartmapsgl.encodeString(apiKey)}`;
            const body = buildRoutingRequest(
                "MATRIX",
                { speedProfile: "FAST" },
                [
                    { type: "Feature", geometry: { type: "Point", coordinates: urgentDeliveryLocation }, properties: { type: "StartPoint" } },
                    { type: "Feature", geometry: { type: "MultiPoint", coordinates: dynamicDriverLocations }, properties: {} }
                ]
            );
            const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
            const data = await response.json();

            const resultContainer = document.getElementById('matrix-result');

            if (data.features?.[0]?.properties?.routingDestinations) {
                const results = data.features[0].properties.routingDestinations;
                if (results.length > 0) {
                    // Find the driver with the shortest travel time
                    const best = results.reduce((prev, curr) => prev.timeInSeconds < curr.timeInSeconds ? prev : curr);
                    const bestDriverIndex = results.indexOf(best);
                    bestDriverInfo = {
                        index: bestDriverIndex,
                        coords: dynamicDriverLocations[bestDriverIndex],
                        time: Math.round(best.timeInSeconds / 60)
                    };

                    resultContainer.innerHTML = `Ergebnis: Fahrer ${bestDriverInfo.index + 1} ist am schnellsten! (ca. ${bestDriverInfo.time} Min.)
                    <button id="track-driver-btn" class="action-button tertiary-button"><i class="material-icons">track_changes</i> Fahrer verfolgen</button>`;
                    document.getElementById('track-driver-btn').addEventListener('click', startDriverAnimation);
                } else { resultContainer.textContent = 'Matrix calculation: no results.'; }
            } else { resultContainer.textContent = 'Matrix calculation failed.'; }

            toggleAccordion(document.getElementById('header-tracking'), true);
        });


        // === DRIVER TRACKING ANIMATION =======================================

        /**
         * Starts the driver tracking animation. Fetches a route from the best
         * driver to the urgent delivery location, hides the control panel, and
         * begins the frame-by-frame truck animation.
         */
        async function startDriverAnimation() {
            const url = `https://www.yellowmap.de/api_rst/v2/geojson/route?apiKey=${smartmapsgl.encodeString(apiKey)}`;
            const requestBody = buildRoutingRequest(
                "ROUTE",
                { speedProfile: "FAST", routingRoundTrip: false, isoLocale: "de-DE", coordFormatOut: "GEODECIMAL_POINT" },
                [
                    { type: "Feature", geometry: { type: "Point", coordinates: bestDriverInfo.coords }, properties: {} },
                    { type: "Feature", geometry: { type: "Point", coordinates: urgentDeliveryLocation }, properties: {} }
                ]
            );

            const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) });
            if (!response.ok) {
                console.error("Route calculation error for tracking:", await response.text());
                return;
            }
            const data = await response.json();
            if (!data.features || data.features.length === 0) {
                console.error("Could not fetch route for driver tracking.");
                return;
            }
            driverRouteGeoJSON = data.features[0];

            // Hide the control panel with a slide-out animation
            controlPanel.style.opacity = 0;
            controlPanel.style.transform = 'translateX(-100%)';
            setTimeout(() => controlPanel.style.display = 'none', 500);
            resetViewBtn.style.display = 'flex';

            // Draw the driver route as a dashed line
            clearMap(true);
            if (map.getSource('driver-route')) {
                map.removeLayer('driver-route-line');
                map.removeSource('driver-route');
            }
            map.addSource('driver-route', { type: 'geojson', data: driverRouteGeoJSON });
            map.addLayer({ id: 'driver-route-line', type: 'line', source: 'driver-route', layout: {}, paint: { 'line-color': getCssVariable('--success-color'), 'line-width': 5, 'line-dasharray': [0, 2] } });

            // Create the truck marker at the driver's starting position
            const truckEl = document.createElement('div');
            truckEl.className = 'truck-marker';
            truckMarker = new smartmapsgl.Marker({ element: truckEl, anchor: 'center' }).setLngLat(bestDriverInfo.coords).addTo(map);

            animateDriver(0);
        }

        /**
         * Animation loop that moves the truck marker along the route coordinates.
         * Uses linear interpolation between route points and smoothly transitions
         * the camera center, zoom, and pitch toward their target values.
         * @param {DOMHighResTimeStamp} timestamp - The current animation frame timestamp
         */
        function animateDriver(timestamp) {
            if (animationFrameId === null) return;
            if (!animationStartTime) animationStartTime = timestamp;

            const progress = Math.min((timestamp - animationStartTime) / TRACKING_ANIMATION_DURATION_MS, 1);
            const routeCoords = driverRouteGeoJSON.geometry.coordinates;

            // Interpolate position along the route polyline
            const exactIndex = progress * (routeCoords.length - 1);
            const startIndex = Math.floor(exactIndex);
            const endIndex = Math.min(startIndex + 1, routeCoords.length - 1);
            const segmentProgress = exactIndex - startIndex;
            const startPos = routeCoords[startIndex];
            const endPos = routeCoords[endIndex];
            const currentLng = startPos[0] + (endPos[0] - startPos[0]) * segmentProgress;
            const currentLat = startPos[1] + (endPos[1] - startPos[1]) * segmentProgress;
            const currentPos = [currentLng, currentLat];

            truckMarker.setLngLat(currentPos);

            // Smooth camera follow using exponential smoothing
            const smoothingWeight = 1 - (1 - TRACKING_SMOOTHING) * 5; // Center follows faster than zoom/pitch
            const smoothedCenter = new smartmapsgl.LngLat(
                map.getCenter().lng * smoothingWeight + currentPos[0] * (1 - smoothingWeight),
                map.getCenter().lat * smoothingWeight + currentPos[1] * (1 - smoothingWeight)
            );
            map.setCenter(smoothedCenter);

            // Gradually transition zoom and pitch toward target values
            map.setZoom(map.getZoom() * TRACKING_SMOOTHING + TRACKING_TARGET_ZOOM * (1 - TRACKING_SMOOTHING));
            map.setPitch(map.getPitch() * TRACKING_SMOOTHING + TRACKING_TARGET_PITCH * (1 - TRACKING_SMOOTHING));

            if (progress < 1) {
                animationFrameId = requestAnimationFrame(animateDriver);
            } else {
                animationStartTime = null;
                animationFrameId = null;
            }
        }


        // === UTILITY FUNCTIONS ===============================================

        /**
         * Generates random driver locations distributed uniformly within a
         * circle of the given radius around a center point.
         * @param {number[]} center - [longitude, latitude] of the center point
         * @param {number} count - Number of driver locations to generate
         * @param {number} radius - Spread radius in kilometers
         * @returns {number[][]} Array of [longitude, latitude] coordinate pairs
         */
        function generateDriverLocations(center, count, radius) {
            const [lon, lat] = center;
            const locations = [];
            const radiusInDegrees = radius / KM_TO_DEGREES;
            for (let i = 0; i < count; i++) {
                const angle = Math.random() * 2 * Math.PI;
                // sqrt ensures uniform distribution within the circle
                const distance = Math.sqrt(Math.random()) * radiusInDegrees;
                const driverLon = lon + (distance * Math.cos(angle)) / Math.cos(lat * Math.PI / 180);
                const driverLat = lat + distance * Math.sin(angle);
                locations.push([driverLon, driverLat]);
            }
            return locations;
        }

        /**
         * Removes all map layers, sources, and markers. Cancels any running
         * animation. When softClear is true, only the driver tracking layer
         * and truck marker are removed (route + isochrone layers are preserved).
         * @param {boolean} [softClear=false] - If true, preserve route/isochrone layers
         */
        function clearMap(softClear = false) {
            if (animationFrameId) {
                cancelAnimationFrame(animationFrameId);
                animationFrameId = null;
                animationStartTime = null;
            }
            if (truckMarker) truckMarker.remove();

            if (map.getSource('driver-route')) { map.removeLayer('driver-route-line'); map.removeSource('driver-route'); }

            if (!softClear) {
                if (map.getSource('route')) { map.removeLayer('route-line'); map.removeSource('route'); }
                if (map.getSource('isochrone')) { map.removeLayer('isochrone-outline'); map.removeLayer('isochrone-area'); map.removeSource('isochrone'); }
                mapMarkers.forEach(marker => marker.remove());
                mapMarkers = [];
                resetAccordions();
            }
        }

        /**
         * Resets the entire view: clears the map, restores the control panel,
         * hides the reset button, and flies back to the default map position.
         */
        function resetView() {
            clearMap();
            controlPanel.style.display = 'flex';
            setTimeout(() => {
                controlPanel.style.opacity = 1;
                controlPanel.style.transform = 'translateX(0)';
            }, 10);
            resetViewBtn.style.display = 'none';
            map.flyTo({ center: { lat: 49.021649, lng: 8.439330 }, zoom: 6, pitch: 0, bearing: 0 });
        }
        resetViewBtn.addEventListener('click', resetView);


        // === ACCORDION UI LOGIC ==============================================

        /**
         * Toggles an accordion section open or closed. Closes all other open
         * sections first (single-open behavior). When forceOpen is true, the
         * section is enabled (if disabled) and opened regardless of current state.
         * @param {HTMLElement} header - The accordion header button element
         * @param {boolean} [forceOpen=false] - Force-open the section and enable it if disabled
         */
        function toggleAccordion(header, forceOpen = false) {
            if (header.disabled && !forceOpen) return;

            if (header.disabled) {
                header.disabled = false;
            }

            const body = header.nextElementSibling;
            const isActive = header.classList.contains('active');

            // Close all other open accordions before opening a new one
            if (!isActive || forceOpen) {
                document.querySelectorAll('.accordion-header.active').forEach(activeHeader => {
                    if (activeHeader !== header) {
                        activeHeader.classList.remove('active');
                        activeHeader.nextElementSibling.style.maxHeight = null;
                    }
                });
            }

            if (!isActive || forceOpen) {
                header.classList.add('active');
                body.style.maxHeight = body.scrollHeight + "px";
            } else {
                header.classList.remove('active');
                body.style.maxHeight = null;
            }
        }

        /**
         * Resets all accordion sections to their initial state: closes everything,
         * disables sections 2-4, and opens section 1 (Tour Planning).
         */
        function resetAccordions() {
            document.querySelectorAll('.accordion-header').forEach((header, index) => {
                header.classList.remove('active');
                header.nextElementSibling.style.maxHeight = null;
                if (index > 0) {
                    header.disabled = true;
                }
            });
            toggleAccordion(document.getElementById('header-planner'), true);
        }

        document.querySelectorAll('.accordion-header').forEach(header => {
            header.addEventListener('click', () => toggleAccordion(header));
        });


        // === DRAG & DROP =====================================================

        /** @type {HTMLElement|null} The DOM element currently being dragged */
        let draggedItem = null;

        /**
         * Handles dragstart on waypoint input groups. Only intermediate stops
         * (draggable elements) can be dragged.
         */
        waypointsContainer.addEventListener('dragstart', (e) => {
            if (e.target.classList.contains('input-group') && e.target.draggable) {
                draggedItem = e.target;
                setTimeout(() => e.target.classList.add('dragging'), 0);
            }
        });

        /** Handles dragend: removes the dragging visual state. */
        waypointsContainer.addEventListener('dragend', () => {
            if (draggedItem) {
                draggedItem.classList.remove('dragging');
                draggedItem = null;
            }
        });

        /**
         * Handles dragover: repositions the dragged element in the DOM based
         * on the cursor's vertical position. Prevents dragging above the first
         * waypoint (start) or below the last (destination).
         */
        waypointsContainer.addEventListener('dragover', (e) => {
            e.preventDefault();
            const draggedEl = document.querySelector('.dragging');
            if (!draggedEl) return;

            const afterElement = getDragAfterElement(waypointsContainer, e.clientY);

            if (afterElement === undefined) {
                waypointsContainer.insertBefore(draggedEl, waypointsContainer.lastElementChild);
            } else {
                // Prevent dropping before the first element (start waypoint)
                if (afterElement === waypointsContainer.firstElementChild) {
                    return;
                }
                waypointsContainer.insertBefore(draggedEl, afterElement);
            }
        });

        /**
         * Handles drop: synchronizes the waypoints array order with the new
         * DOM order after a drag-and-drop reorder operation.
         */
        waypointsContainer.addEventListener('drop', (e) => {
            e.preventDefault();
             if (draggedItem) {
                const newOrderWaypoints = [];
                const domInputGroups = Array.from(waypointsContainer.querySelectorAll('.input-group'));

                domInputGroups.forEach(group => {
                    const waypointObject = waypoints.find(wp => wp.group === group);
                    if (waypointObject) {
                        newOrderWaypoints.push(waypointObject);
                    }
                });

                waypoints = newOrderWaypoints;
                updateWaypointPlaceholders();
                checkCalculable();
            }
        });

        /**
         * Determines which element the dragged item should be inserted before,
         * based on the vertical cursor position relative to each child's midpoint.
         * @param {HTMLElement} container - The waypoints container
         * @param {number} y - The current cursor Y position
         * @returns {HTMLElement|undefined} The element to insert before, or undefined for end
         */
        function getDragAfterElement(container, y) {
            const draggableElements = [...container.querySelectorAll('.input-group:not(.dragging)')];
            return draggableElements.reduce((closest, child) => {
                const box = child.getBoundingClientRect();
                const offset = y - box.top - box.height / 2;
                if (offset < 0 && offset > closest.offset) {
                    return { offset: offset, element: child };
                } else {
                    return closest;
                }
            }, { offset: Number.NEGATIVE_INFINITY }).element;
        }


        // === INITIALIZATION ==================================================

        // Initialize autocomplete on all existing waypoint inputs
        document.querySelectorAll('.waypoint-input').forEach(input => initializeWaypointInput(input));

        // Wire up button event listeners
        addWaypointBtn.addEventListener('click', addWaypoint);
        calculateBtn.addEventListener('click', calculateRoute);

        // Transport mode toggle buttons
        transportButtons.forEach(button => {
            button.addEventListener('click', () => {
                transportButtons.forEach(btn => btn.classList.remove('active'));
                button.classList.add('active');
                currentSpeedProfile = button.dataset.profile;
            });
        });

        // Toggle the "optimize" checkbox when clicking anywhere on the options row
        document.querySelector('.options').addEventListener('click', (e) => {
            if (e.target.id !== 'optimize-checkbox') {
                optimizeCheckbox.checked = !optimizeCheckbox.checked;
            }
        });

        // Set initial accordion and placeholder state
        resetAccordions();
        updateWaypointPlaceholders();

    </script>
</body>
</html>