Route progress

Open in CodeSandbox

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
        <script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
        <script crossorigin src="https://cdn.jsdelivr.net/npm/@turf/turf@7.1"></script>

        <!-- To make the map appear, you must add your apikey -->
        <script src="https://js.api.mappable.world/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></script>

        <script
            data-plugins="transform-modules-umd"
            data-presets="typescript"
            type="text/babel"
            src="./common.ts"
        ></script>
        <script
            data-plugins="transform-modules-umd"
            data-presets="typescript"
            type="text/babel"
            src="../variables.ts"
        ></script>
        <script data-plugins="transform-modules-umd" data-presets="typescript" type="text/babel">
            import type {LngLat} from '@mappable-world/mappable-types';
            import {
                ANIMATE_DURATION_MS,
                DriverAnimation,
                angleFromCoordinate,
                animate,
                fetchRoute,
                splitLineString
            } from './common';
            import {
                INITIAL_DRIVER_SPEED,
                LOCATION,
                MARKER_IMAGE_PATH,
                MAX_DRIVER_SPEED,
                MIN_DRIVER_SPEED,
                PASSED_ROUTE_STYLE,
                ROUTE,
                ROUTE_STYLE
            } from '../variables';
            
            window.map = null;
            
            main();
            
            async function main() {
                // Waiting for all api elements to be loaded
                await mappable.ready;
                const {MMap, MMapDefaultSchemeLayer, MMapDefaultFeaturesLayer, MMapFeature, MMapMarker, MMapControls, MMapControl} =
                    mappable;
            
                // Import the package to add a default marker
                const {MMapDefaultMarker} = await mappable.import('@mappable-world/mappable-default-ui-theme');
            
                // Initialize the map
                map = new MMap(
                    // Pass the link to the HTMLElement of the container
                    document.getElementById('app'),
                    // Pass the map initialization parameters
                    {location: LOCATION, showScaleInCopyrights: true},
                    [
                        // Add a map scheme layer
                        new MMapDefaultSchemeLayer({}),
                        // Add a layer of geo objects to display the markers
                        new MMapDefaultFeaturesLayer({})
                    ]
                );
            
                class ResetButton extends mappable.MMapComplexEntity<{onClick: () => void}> {
                    private _element!: HTMLButtonElement;
            
                    // Method for create a DOM control element
                    _createElement() {
                        // Create a root element
                        const button = document.createElement('button');
                        button.classList.add('button');
                        button.innerText = 'Restart';
                        return button;
                    }
            
                    // Method for attaching the control to the map
                    _onAttach() {
                        this._element = this._createElement();
                        this._element.addEventListener('click', this._onClick);
            
                        const control = new MMapControl({}, this._element);
                        this.addChild(control);
                    }
            
                    // Method for detaching control from the map
                    _onDetach() {
                        this._element.removeEventListener('click', this._onClick);
                    }
            
                    _onClick = () => {
                        this._props.onClick();
                    };
                }
                type SpeedRangeProps = {
                    onChange: (value: number) => void;
                    initialValue: number;
                    min: number;
                    max: number;
                };
                class SpeedRange extends mappable.MMapComplexEntity<SpeedRangeProps> {
                    private _element!: HTMLDivElement;
                    private _input!: HTMLInputElement;
            
                    // Method for create a DOM control element
                    _createElement() {
                        // Create a root element
                        const container = document.createElement('div');
                        container.classList.add('container');
            
                        const text = document.createElement('div');
                        text.classList.add('text');
                        text.innerText = 'speed';
            
                        this._input = document.createElement('input');
                        this._input.id = 'range';
                        this._input.type = 'range';
                        this._input.min = this._props.min.toString();
                        this._input.max = this._props.max.toString();
                        this._input.step = '1';
                        this._input.value = this._props.initialValue.toString();
                        this._input.classList.add('slider');
                        const percent = this.__getPercent(this._props.initialValue);
                        this._input.style.background = `linear-gradient(to right, #122DB2 ${percent}%, #F5F6F7 ${percent}%)`;
                        this._input.addEventListener('input', this._onInput);
            
                        container.appendChild(text);
                        container.appendChild(this._input);
            
                        return container;
                    }
            
                    __getPercent(value: number) {
                        return ((value - this._props.min) / (this._props.max - this._props.min)) * 100;
                    }
            
                    // Method for attaching the control to the map
                    _onAttach() {
                        this._element = this._createElement();
                        const control = new MMapControl({transparent: true}, this._element);
                        this.addChild(control);
                    }
            
                    // Method for detaching control from the map
                    _onDetach() {
                        this._input.removeEventListener('input', this._onInput);
                    }
            
                    _onInput = () => {
                        const value = Number(this._input.value);
                        this._props.onChange(value);
                        const percent = this.__getPercent(value);
                        this._input.style.background = `linear-gradient(to right, #122DB2 ${percent}%, #F5F6F7 ${percent}%)`;
                    };
                }
            
                let animation: DriverAnimation;
                let driverSpeed = INITIAL_DRIVER_SPEED;
                let prevCoordinates: LngLat;
            
                const routeProgress = (initDistance: number) => {
                    let passedDistance = initDistance;
                    let passedTime = 0;
                    animation = animate((progress) => {
                        const timeS = (progress * ANIMATE_DURATION_MS) / 1000;
                        const length = passedDistance + driverSpeed * (timeS - passedTime);
            
                        const nextCoordinates = turf.along(route.geometry, length, {units: 'meters'}).geometry
                            .coordinates as LngLat;
            
                        marker.update({coordinates: nextCoordinates});
                        if (prevCoordinates && !turf.booleanEqual(turf.point(prevCoordinates), turf.point(nextCoordinates))) {
                            const angle = angleFromCoordinate(prevCoordinates, nextCoordinates);
                            const markerElement = document.getElementById('marker');
                            markerElement.style.transform = `rotate(${angle}deg)`;
                        }
            
                        const [newLineStingFirstPart, newLineStringSecondPart] = splitLineString(route, nextCoordinates);
                        lineStringFirstPart.update({geometry: newLineStingFirstPart});
                        lineStringSecondPart.update({geometry: newLineStringSecondPart});
            
                        prevCoordinates = nextCoordinates;
                        passedTime = timeS;
                        passedDistance = length;
            
                        if (progress === 1 && routeLength > length) {
                            routeProgress(length);
                        }
                    });
                };
            
                const lineStringSecondPart = new MMapFeature({
                    geometry: {coordinates: [], type: 'LineString'},
                    style: PASSED_ROUTE_STYLE
                });
            
                const lineStringFirstPart = new MMapFeature({
                    geometry: {coordinates: [], type: 'LineString'},
                    style: ROUTE_STYLE
                });
            
                map.addChild(new MMapDefaultMarker(ROUTE.start));
                map.addChild(new MMapDefaultMarker(ROUTE.end));
            
                const markerElement = document.createElement('div');
                markerElement.classList.add('marker_container');
            
                const markerElementImg = document.createElement('img');
                markerElementImg.src = MARKER_IMAGE_PATH;
                markerElementImg.alt = 'marker';
                markerElementImg.id = 'marker';
                markerElement.appendChild(markerElementImg);
            
                const marker = new MMapMarker(
                    {
                        coordinates: ROUTE.start.coordinates,
                        disableRoundCoordinates: true
                    },
                    markerElement
                );
                map.addChild(marker);
            
                map.addChild(
                    new MMapControls({position: 'bottom'}, [
                        new ResetButton({
                            onClick: () => {
                                const animationId = animation.getAnimationId();
                                cancelAnimationFrame(animationId);
                                marker.update({coordinates: ROUTE.start.coordinates});
                                routeProgress(0);
                            }
                        })
                    ])
                ).addChild(
                    new MMapControls({position: 'top right'}, [
                        new SpeedRange({
                            initialValue: INITIAL_DRIVER_SPEED,
                            min: MIN_DRIVER_SPEED,
                            max: MAX_DRIVER_SPEED,
                            onChange: (value) => {
                                driverSpeed = value;
                            }
                        })
                    ])
                );
            
                const route = await fetchRoute(ROUTE.start.coordinates, ROUTE.end.coordinates);
                const routeLength = turf.length(turf.lineString(route.geometry.coordinates), {units: 'meters'});
                lineStringFirstPart.update({geometry: route.geometry});
                map.addChild(lineStringFirstPart);
                map.addChild(lineStringSecondPart);
                routeProgress(0);
            }
        </script>

        <style> html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; } .toolbar { position: absolute; z-index: 1000; top: 0; left: 0; display: flex; align-items: center; padding: 16px; } .toolbar a { padding: 16px; }  </style>
        <link rel="stylesheet" href="./common.css" />
    </head>
    <body>
        <div id="app"></div>
    </body>
</html>
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
        <script crossorigin src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js"></script>
        <script crossorigin src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js"></script>
        <script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
        <script crossorigin src="https://cdn.jsdelivr.net/npm/@turf/turf@7.1"></script>

        <!-- To make the map appear, you must add your apikey -->
        <script src="https://js.api.mappable.world/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></script>

        <script
            data-plugins="transform-modules-umd"
            data-presets="react, typescript"
            type="text/babel"
            src="./common.ts"
        ></script>
        <script
            data-plugins="transform-modules-umd"
            data-presets="react, typescript"
            type="text/babel"
            src="../variables.ts"
        ></script>
        <script data-plugins="transform-modules-umd" data-presets="react, typescript" type="text/babel">
            import type {LineStringGeometry, LngLat, RouteFeature} from '@mappable-world/mappable-types';
            import {
                ANIMATE_DURATION_MS,
                DriverAnimation,
                angleFromCoordinate,
                animate,
                fetchRoute,
                splitLineString
            } from './common';
            import {
                INITIAL_DRIVER_SPEED,
                LOCATION,
                MARKER_IMAGE_PATH,
                MAX_DRIVER_SPEED,
                MIN_DRIVER_SPEED,
                PASSED_ROUTE_STYLE,
                ROUTE,
                ROUTE_STYLE
            } from '../variables';
            import type {ChangeEvent, CSSProperties} from 'react';
            
            window.map = null;
            
            main();
            async function main() {
                // For each object in the JS API, there is a React counterpart
                // To use the React version of the API, include the module @mappable-world/mappable-reactify
                const [mappableReact] = await Promise.all([mappable.import('@mappable-world/mappable-reactify'), mappable.ready]);
                const reactify = mappableReact.reactify.bindTo(React, ReactDOM);
                const {MMap, MMapDefaultSchemeLayer, MMapDefaultFeaturesLayer, MMapMarker, MMapFeature, MMapControls, MMapControl} =
                    reactify.module(mappable);
            
                // Import the package to add a default marker
                const {MMapDefaultMarker} = await reactify.module(await mappable.import('@mappable-world/mappable-default-ui-theme'));
            
                const {useState, useMemo, useEffect, useRef, useCallback} = React;
            
                const getPercent = (value: number) => {
                    return ((value - MIN_DRIVER_SPEED) / (MAX_DRIVER_SPEED - MIN_DRIVER_SPEED)) * 100;
                };
            
                function App() {
                    const animation = useRef<DriverAnimation>();
                    const route = useRef<RouteFeature>();
                    const routeLength = useRef<number>(0);
                    const prevCoordinates = useRef<LngLat>();
                    const driverSpeedRef = useRef(INITIAL_DRIVER_SPEED);
            
                    const [coordinates, setCoordinates] = useState(ROUTE.start.coordinates);
                    const [angle, setAngle] = useState(0);
                    const [lineStringSecondPart, setLineStringSecondPart] = useState<LineStringGeometry>({
                        type: 'LineString',
                        coordinates: []
                    });
                    const [lineStringFirstPart, setLineStringFirstPart] = useState<LineStringGeometry>({
                        type: 'LineString',
                        coordinates: []
                    });
            
                    const [sliderStyle, setSliderStyle] = useState<CSSProperties>({
                        background: `linear-gradient(to right, #122DB2 ${getPercent(driverSpeedRef.current)}%, #F5F6F7 ${getPercent(
                            driverSpeedRef.current
                        )}%)`
                    });
            
                    const onRestartClick = useCallback(() => {
                        const animationId = animation.current.getAnimationId();
                        cancelAnimationFrame(animationId);
                        setCoordinates(ROUTE.start.coordinates);
                        routeProgress(0);
                    }, []);
            
                    const onSliderChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
                        const value = Number(event.target.value);
                        driverSpeedRef.current = value;
                        setSliderStyle({
                            background: `linear-gradient(to right, #122DB2 ${getPercent(value)}%, #F5F6F7 ${getPercent(value)}%)`
                        });
                    }, []);
            
                    useEffect(() => {
                        fetchRoute(ROUTE.start.coordinates, ROUTE.end.coordinates).then((routeRes) => {
                            route.current = routeRes;
                            routeLength.current = turf.length(turf.lineString(route.current.geometry.coordinates), {
                                units: 'meters'
                            });
                            setLineStringFirstPart(route.current.geometry);
                            routeProgress(0);
                        });
                    }, []);
            
                    const routeProgress = (initDistance: number) => {
                        let passedDistance = initDistance;
                        let passedTime = 0;
                        animation.current = animate((progress) => {
                            const timeS = (progress * ANIMATE_DURATION_MS) / 1000;
                            const length = passedDistance + driverSpeedRef.current * (timeS - passedTime);
            
                            const nextCoordinates = turf.along(route.current.geometry, length, {units: 'meters'}).geometry
                                .coordinates as LngLat;
            
                            setCoordinates(nextCoordinates);
                            if (
                                prevCoordinates.current &&
                                !turf.booleanEqual(turf.point(prevCoordinates.current), turf.point(nextCoordinates))
                            ) {
                                setAngle(angleFromCoordinate(prevCoordinates.current, nextCoordinates));
                            }
            
                            const [newLineStingFirstPart, newLineStringSecondPart] = splitLineString(
                                route.current,
                                nextCoordinates
                            );
                            setLineStringFirstPart(newLineStingFirstPart);
                            setLineStringSecondPart(newLineStringSecondPart);
            
                            prevCoordinates.current = nextCoordinates;
                            passedTime = timeS;
                            passedDistance = length;
            
                            if (progress === 1 && routeLength.current > length) {
                                routeProgress(length);
                            }
                        });
                    };
            
                    return (
                        // Initialize the map and pass initialization parameters
                        <MMap location={LOCATION} showScaleInCopyrights={true} ref={(x) => (map = x)}>
                            {/* Add a map scheme layer */}
                            <MMapDefaultSchemeLayer />
                            {/* Add a layer of geo objects to display the markers */}
                            <MMapDefaultFeaturesLayer />
            
                            <MMapDefaultMarker {...ROUTE.start} />
                            <MMapDefaultMarker {...ROUTE.end} />
            
                            <MMapFeature geometry={lineStringFirstPart} style={ROUTE_STYLE} />
                            <MMapFeature geometry={lineStringSecondPart} style={PASSED_ROUTE_STYLE} />
            
                            <MMapMarker disableRoundCoordinates coordinates={coordinates}>
                                <div className="marker_container">
                                    <img src={MARKER_IMAGE_PATH} alt="marker" style={{transform: `rotate(${angle}deg)`}} />
                                </div>
                            </MMapMarker>
            
                            <MMapControls position="bottom">
                                <MMapControl>
                                    <button onClick={onRestartClick} className="button">
                                        Restart
                                    </button>
                                </MMapControl>
                            </MMapControls>
                            <MMapControls position="top right">
                                <MMapControl transparent={true}>
                                    <div className="container">
                                        <div className="text">speed</div>
                                        <input
                                            style={sliderStyle}
                                            type="range"
                                            defaultValue={INITIAL_DRIVER_SPEED}
                                            min={MIN_DRIVER_SPEED}
                                            onChange={onSliderChange}
                                            max={MAX_DRIVER_SPEED}
                                            step="1"
                                            className="slider"
                                        />
                                    </div>
                                </MMapControl>
                            </MMapControls>
                        </MMap>
                    );
                }
            
                ReactDOM.render(
                    <React.StrictMode>
                        <App />
                    </React.StrictMode>,
                    document.getElementById('app')
                );
            }
        </script>

        <style> html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; } .toolbar { position: absolute; z-index: 1000; top: 0; left: 0; display: flex; align-items: center; padding: 16px; } .toolbar a { padding: 16px; }  </style>
        <link rel="stylesheet" href="./common.css" />
    </head>
    <body>
        <div id="app"></div>
    </body>
</html>
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
        <script crossorigin src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
        <script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
        <script crossorigin src="https://cdn.jsdelivr.net/npm/@turf/turf@7.1"></script>

        <script src="https://js.api.mappable.world/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></script>

        <script
            data-plugins="transform-modules-umd"
            data-presets="typescript"
            type="text/babel"
            src="./common.ts"
        ></script>
        <script
            data-plugins="transform-modules-umd"
            data-presets="typescript"
            type="text/babel"
            src="../variables.ts"
        ></script>
        <script data-plugins="transform-modules-umd" data-presets="typescript" type="text/babel">
            import type {LineStringGeometry, LngLat, RouteFeature} from '@mappable-world/mappable-types';
            import {
                ANIMATE_DURATION_MS,
                DriverAnimation,
                angleFromCoordinate,
                animate,
                fetchRoute,
                splitLineString
            } from './common';
            import {
                INITIAL_DRIVER_SPEED,
                LOCATION,
                MARKER_IMAGE_PATH,
                MAX_DRIVER_SPEED,
                MIN_DRIVER_SPEED,
                PASSED_ROUTE_STYLE,
                ROUTE,
                ROUTE_STYLE
            } from '../variables';
            
            window.map = null;
            
            async function main() {
                // For each object in the JS API, there is a Vue counterpart
                // To use the Vue version of the API, include the module @mappable-world/mappable-vuefy
                const [mappableVue] = await Promise.all([mappable.import('@mappable-world/mappable-vuefy'), mappable.ready]);
                const vuefy = mappableVue.vuefy.bindTo(Vue);
                const {MMap, MMapDefaultSchemeLayer, MMapDefaultFeaturesLayer, MMapFeature, MMapMarker, MMapControls, MMapControl} =
                    vuefy.module(mappable);
            
                // Import the package to add a default marker
                const {MMapDefaultMarker} = await vuefy.module(await mappable.import('@mappable-world/mappable-default-ui-theme'));
            
                const {ref, onMounted} = Vue;
            
                const getPercent = (value: number) => {
                    return ((value - MIN_DRIVER_SPEED) / (MAX_DRIVER_SPEED - MIN_DRIVER_SPEED)) * 100;
                };
            
                const app = Vue.createApp({
                    components: {
                        MMap,
                        MMapDefaultSchemeLayer,
                        MMapDefaultFeaturesLayer,
                        MMapFeature,
                        MMapDefaultMarker,
                        MMapMarker,
                        MMapControls,
                        MMapControl
                    },
                    setup() {
                        const refMap = (ref) => {
                            window.map = ref?.entity;
                        };
            
                        let animation: DriverAnimation;
                        let route: RouteFeature;
                        let routeLength = 0;
                        let prevCoordinates: LngLat;
            
                        const driverSpeed = ref(INITIAL_DRIVER_SPEED);
                        const coordinates = ref(ROUTE.start.coordinates);
                        const angle = ref(0);
                        const lineStringSecondPart = ref<LineStringGeometry>({
                            type: 'LineString',
                            coordinates: []
                        });
                        const lineStringFirstPart = ref<LineStringGeometry>({
                            type: 'LineString',
                            coordinates: []
                        });
                        const sliderStyle = ref({
                            background: `linear-gradient(to right, #122DB2 ${getPercent(driverSpeed.value)}%, #F5F6F7 ${getPercent(
                                driverSpeed.value
                            )}%)`
                        });
            
                        const onRestartClick = () => {
                            const animationId = animation.getAnimationId();
                            cancelAnimationFrame(animationId);
                            coordinates.value = ROUTE.start.coordinates;
                            routeProgress(0);
                        };
            
                        const onSliderChange = (event: Event) => {
                            const value = Number((event.target as HTMLInputElement).value);
                            sliderStyle.value = {
                                background: `linear-gradient(to right, #122DB2 ${getPercent(value)}%, #F5F6F7 ${getPercent(
                                    value
                                )}%)`
                            };
                        };
            
                        onMounted(() => {
                            fetchRoute(ROUTE.start.coordinates, ROUTE.end.coordinates).then((routeRes) => {
                                route = routeRes;
                                routeLength = turf.length(turf.lineString(route.geometry.coordinates), {
                                    units: 'meters'
                                });
                                lineStringFirstPart.value = route.geometry;
                                routeProgress(0);
                            });
                        });
            
                        const routeProgress = (initDistance: number) => {
                            let passedDistance = initDistance;
                            let passedTime = 0;
                            animation = animate((progress) => {
                                const timeS = (progress * ANIMATE_DURATION_MS) / 1000;
                                const length = passedDistance + driverSpeed.value * (timeS - passedTime);
            
                                const nextCoordinates = turf.along(route.geometry, length, {units: 'meters'}).geometry
                                    .coordinates as LngLat;
            
                                coordinates.value = nextCoordinates;
                                if (
                                    prevCoordinates &&
                                    !turf.booleanEqual(turf.point(prevCoordinates), turf.point(nextCoordinates))
                                ) {
                                    angle.value = angleFromCoordinate(prevCoordinates, nextCoordinates);
                                }
            
                                const [newLineStingFirstPart, newLineStringSecondPart] = splitLineString(route, nextCoordinates);
                                lineStringFirstPart.value = newLineStingFirstPart;
                                lineStringSecondPart.value = newLineStringSecondPart;
            
                                prevCoordinates = nextCoordinates;
                                passedTime = timeS;
                                passedDistance = length;
            
                                if (progress === 1 && routeLength > length) {
                                    routeProgress(length);
                                }
                            });
                        };
            
                        return {
                            LOCATION,
                            ROUTE,
                            ROUTE_STYLE,
                            PASSED_ROUTE_STYLE,
                            MARKER_IMAGE_PATH,
                            INITIAL_DRIVER_SPEED,
                            MAX_DRIVER_SPEED,
                            MIN_DRIVER_SPEED,
                            refMap,
                            driverSpeed,
                            coordinates,
                            lineStringSecondPart,
                            lineStringFirstPart,
                            sliderStyle,
                            angle,
                            onRestartClick,
                            onSliderChange
                        };
                    },
                    template: `
                      <!--Initialize the map and pass initialization parameters-->
                      <MMap :location="LOCATION" :showScaleInCopyrights="true" :ref="refMap">
                        <!--Add a map scheme layer-->
                        <MMapDefaultSchemeLayer/>
                        <!-- Add a layer of geo objects to display the markers -->
                        <MMapDefaultFeaturesLayer/>
            
                        <MMapDefaultMarker v-bind="ROUTE.start" />
                        <MMapDefaultMarker v-bind="ROUTE.end" />
            
                        <MMapFeature :geometry="lineStringFirstPart" :style="ROUTE_STYLE"/>
                        <MMapFeature :geometry="lineStringSecondPart" :style="PASSED_ROUTE_STYLE" />
            
                        <MMapMarker disableRoundCoordinates :coordinates="coordinates">
                          <div class="marker_container">
                            <img
                              :src="MARKER_IMAGE_PATH"
                              alt="marker"
                              :style="{ transform: 'rotate(' + angle + 'deg)' }"
                            />
                          </div>
                        </MMapMarker>
            
                        <MMapControls position="bottom">
                          <MMapControl>
                            <button @click="onRestartClick" class="button">Restart</button>
                          </MMapControl>
                        </MMapControls>
            
                        <MMapControls position="top right">
                          <MMapControl :transparent="true">
                            <div class="container">
                              <div class="text">
                                speed
                              </div>
                              <input
                                type="range"
                                v-model="driverSpeed"
                                :min="MIN_DRIVER_SPEED"
                                @input="onSliderChange"
                                :max="MAX_DRIVER_SPEED"
                                step="1"
                                class="slider"
                                :style="sliderStyle"
                              />
                            </div>
                          </MMapControl>
                        </MMapControls>
                      </MMap>`
                });
                app.mount('#app');
            }
            main();
        </script>

        <style> html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; } .toolbar { position: absolute; z-index: 1000; top: 0; left: 0; display: flex; align-items: center; padding: 16px; } .toolbar a { padding: 16px; }  </style>
        <link rel="stylesheet" href="./common.css" />
    </head>
    <body>
        <div id="app"></div>
    </body>
</html>
import type {DrawingStyle, MMapLocationRequest} from '@mappable-world/mappable-types';
import type {MMapDefaultMarkerProps} from '@mappable-world/mappable-default-ui-theme';

export const LOCATION: MMapLocationRequest = {
    center: [55.4355, 25.3461], // starting position [lng, lat]
    zoom: 14.0 // starting zoom
};

export const INITIAL_DRIVER_SPEED = 210;
export const MIN_DRIVER_SPEED = 20;
export const MAX_DRIVER_SPEED = 400;

export const ROUTE: {start: Partial<MMapDefaultMarkerProps>; end: Partial<MMapDefaultMarkerProps>} = {
    start: {
        zIndex: 1000,
        size: 'small',
        title: 'Home',
        iconName: 'building',
        coordinates: [55.4609, 25.3398]
    },
    end: {
        zIndex: 1000,
        size: 'normal',
        title: 'Al Hisn Fort',
        iconName: 'fallback',
        coordinates: [55.3862, 25.3587]
    }
};

export const ROUTE_STYLE: DrawingStyle = {
    simplificationRate: 0,
    stroke: [
        {color: '#34D9AD', width: 7},
        {color: '#050D33', opacity: 0.4, width: 9}
    ]
};

export const PASSED_ROUTE_STYLE: DrawingStyle = {
    simplificationRate: 0,
    stroke: [{color: '#050D33', opacity: 0.4, width: 9}]
};

export const MARKER_IMAGE_PATH = '../marker-black.png';
import type {LineStringGeometry, LngLat, RouteFeature} from '@mappable-world/mappable-types';

// Wait for the api to load to access the map configuration
mappable.ready.then(() => {
    // 
    mappable.import.registerCdn('https://cdn.jsdelivr.net/npm/{package}', '@mappable-world/mappable-default-ui-theme@0.0');
});

export async function fetchRoute(startCoordinates: LngLat, endCoordinates: LngLat) {
    // Request a route from the Router API with the specified parameters.
    const routes = await mappable.route({
        points: [startCoordinates, endCoordinates], // Start and end points of the route LngLat[]
        type: 'driving', // Type of the route
        bounds: true // Flag indicating whether to include route boundaries in the response
    });

    // Check if a route was found
    if (!routes[0]) return;

    // Convert the received route to a RouteFeature object.
    const route = routes[0].toRoute();

    // Check if a route has coordinates
    if (route.geometry.coordinates.length == 0) return;

    return route;
}

export const ANIMATE_DURATION_MS = 4000;
export type DriverAnimation = {
    getAnimationId: () => number;
};

export function animate(cb: (progress: number) => void): DriverAnimation {
    let animationId = 0;
    const startTime = Date.now();
    function tick() {
        const progress = (Date.now() - startTime) / ANIMATE_DURATION_MS;
        if (progress >= 1) {
            cb(1);
            return;
        }

        cb(progress);
        animationId = requestAnimationFrame(tick);
    }

    animationId = requestAnimationFrame(tick);

    return {
        getAnimationId: () => animationId
    };
}

export function angleFromCoordinate(lngLat1: LngLat, lngLat2: LngLat) {
    const toRadians = (degrees: number) => degrees * (Math.PI / 180);
    const toDegrees = (radians: number) => radians * (180 / Math.PI);

    const dLon = toRadians(lngLat2[0] - lngLat1[0]);

    const y = Math.sin(dLon) * Math.cos(toRadians(lngLat2[1]));
    const x =
        Math.cos(toRadians(lngLat1[1])) * Math.sin(toRadians(lngLat2[1])) -
        Math.sin(toRadians(lngLat1[1])) * Math.cos(toRadians(lngLat2[1])) * Math.cos(dLon);

    let deg = Math.atan2(y, x);

    deg = toDegrees(deg);
    deg = (deg + 360) % 360;

    return deg;
}

export function splitLineString(route: RouteFeature, coordinates: LngLat) {
    if (!route || !coordinates) {
        return [];
    }
    const firstPart = turf.lineSlice(
        coordinates,
        route.geometry.coordinates[route.geometry.coordinates.length - 1],
        route.geometry
    );
    const secondPart = turf.lineSlice(route.geometry.coordinates[0], coordinates, route.geometry);
    return [firstPart.geometry as LineStringGeometry, secondPart.geometry as LineStringGeometry];
}
.marker_container {
    position: absolute;

    transform: translate(-50%, -50%) scale(0.5);
}

.container {
    display: flex;
    align-items: center;

    width: 210px;
    padding: 16px;

    border-radius: 12px;
    background: #fff;
    box-shadow: 0 4px 12px 0 rgba(95, 105, 131, 0.1), 0 4px 24px 0 rgba(95, 105, 131, 0.04);
    gap: 12px;
}

.text {
    font-size: 14px;
    font-style: normal;
    line-height: 16px;

    color: #050d33;
}

.button {
    width: 120px;
    height: 40px;
    margin: 0 auto;

    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    text-align: center;

    border: none;
    border-radius: 12px;
    background-color: #fff;
}

input[type='range'] {
    width: 100%;
    height: 2px;

    cursor: pointer;

    outline: none;
    background: linear-gradient(to right, #122db2 50%, #f5f6f7 50%);
    -webkit-appearance: none;
    appearance: none;
}

input[type='range']::-webkit-slider-thumb {
    width: 16px;
    height: 16px;

    cursor: pointer;

    border: 2px solid #122db2;
    border-radius: 50%;
    background-color: #fff;
    -webkit-appearance: none;
    appearance: none;
}