Прогресс по маршруту

<!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://api-maps.yandex.ru/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 '@yandex/ymaps3-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 ymaps3.ready;
                const {YMap, YMapDefaultSchemeLayer, YMapDefaultFeaturesLayer, YMapFeature, YMapMarker, YMapControls, YMapControl} =
                    ymaps3;
            
                // Import the package to add a default marker
                const {YMapDefaultMarker} = await ymaps3.import('@yandex/ymaps3-default-ui-theme');
            
                // Initialize the map
                map = new YMap(
                    // 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 YMapDefaultSchemeLayer({}),
                        // Add a layer of geo objects to display the markers
                        new YMapDefaultFeaturesLayer({})
                    ]
                );
            
                class ResetButton extends ymaps3.YMapComplexEntity<{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 YMapControl({}, 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 ymaps3.YMapComplexEntity<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 YMapControl({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 YMapFeature({
                    geometry: {coordinates: [], type: 'LineString'},
                    style: PASSED_ROUTE_STYLE
                });
            
                const lineStringFirstPart = new YMapFeature({
                    geometry: {coordinates: [], type: 'LineString'},
                    style: ROUTE_STYLE
                });
            
                map.addChild(new YMapDefaultMarker(ROUTE.start));
                map.addChild(new YMapDefaultMarker(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 YMapMarker(
                    {
                        coordinates: ROUTE.start.coordinates,
                        disableRoundCoordinates: true
                    },
                    markerElement
                );
                map.addChild(marker);
            
                map.addChild(
                    new YMapControls({position: 'bottom'}, [
                        new ResetButton({
                            onClick: () => {
                                const animationId = animation.getAnimationId();
                                cancelAnimationFrame(animationId);
                                marker.update({coordinates: ROUTE.start.coordinates});
                                routeProgress(0);
                            }
                        })
                    ])
                ).addChild(
                    new YMapControls({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://api-maps.yandex.ru/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 '@yandex/ymaps3-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 @yandex/ymaps3-reactify
                const [ymaps3React] = await Promise.all([ymaps3.import('@yandex/ymaps3-reactify'), ymaps3.ready]);
                const reactify = ymaps3React.reactify.bindTo(React, ReactDOM);
                const {YMap, YMapDefaultSchemeLayer, YMapDefaultFeaturesLayer, YMapMarker, YMapFeature, YMapControls, YMapControl} =
                    reactify.module(ymaps3);
            
                // Import the package to add a default marker
                const {YMapDefaultMarker} = await reactify.module(await ymaps3.import('@yandex/ymaps3-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
                        <YMap location={LOCATION} showScaleInCopyrights={true} ref={(x) => (map = x)}>
                            {/* Add a map scheme layer */}
                            <YMapDefaultSchemeLayer />
                            {/* Add a layer of geo objects to display the markers */}
                            <YMapDefaultFeaturesLayer />
            
                            <YMapDefaultMarker {...ROUTE.start} />
                            <YMapDefaultMarker {...ROUTE.end} />
            
                            <YMapFeature geometry={lineStringFirstPart} style={ROUTE_STYLE} />
                            <YMapFeature geometry={lineStringSecondPart} style={PASSED_ROUTE_STYLE} />
            
                            <YMapMarker disableRoundCoordinates coordinates={coordinates}>
                                <div className="marker_container">
                                    <img src={MARKER_IMAGE_PATH} alt="marker" style={{transform: `rotate(${angle}deg)`}} />
                                </div>
                            </YMapMarker>
            
                            <YMapControls position="bottom">
                                <YMapControl>
                                    <button onClick={onRestartClick} className="button">
                                        Restart
                                    </button>
                                </YMapControl>
                            </YMapControls>
                            <YMapControls position="top right">
                                <YMapControl 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>
                                </YMapControl>
                            </YMapControls>
                        </YMap>
                    );
                }
            
                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://api-maps.yandex.ru/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 '@yandex/ymaps3-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 @yandex/ymaps3-vuefy
                const [ymaps3Vue] = await Promise.all([ymaps3.import('@yandex/ymaps3-vuefy'), ymaps3.ready]);
                const vuefy = ymaps3Vue.vuefy.bindTo(Vue);
                const {YMap, YMapDefaultSchemeLayer, YMapDefaultFeaturesLayer, YMapFeature, YMapMarker, YMapControls, YMapControl} =
                    vuefy.module(ymaps3);
            
                // Import the package to add a default marker
                const {YMapDefaultMarker} = await vuefy.module(await ymaps3.import('@yandex/ymaps3-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: {
                        YMap,
                        YMapDefaultSchemeLayer,
                        YMapDefaultFeaturesLayer,
                        YMapFeature,
                        YMapDefaultMarker,
                        YMapMarker,
                        YMapControls,
                        YMapControl
                    },
                    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-->
                      <YMap :location="LOCATION" :showScaleInCopyrights="true" :ref="refMap">
                        <!--Add a map scheme layer-->
                        <YMapDefaultSchemeLayer/>
                        <!-- Add a layer of geo objects to display the markers -->
                        <YMapDefaultFeaturesLayer/>
            
                        <YMapDefaultMarker v-bind="ROUTE.start" />
                        <YMapDefaultMarker v-bind="ROUTE.end" />
            
                        <YMapFeature :geometry="lineStringFirstPart" :style="ROUTE_STYLE"/>
                        <YMapFeature :geometry="lineStringSecondPart" :style="PASSED_ROUTE_STYLE" />
            
                        <YMapMarker disableRoundCoordinates :coordinates="coordinates">
                          <div class="marker_container">
                            <img
                              :src="MARKER_IMAGE_PATH"
                              alt="marker"
                              :style="{ transform: 'rotate(' + angle + 'deg)' }"
                            />
                          </div>
                        </YMapMarker>
            
                        <YMapControls position="bottom">
                          <YMapControl>
                            <button @click="onRestartClick" class="button">Restart</button>
                          </YMapControl>
                        </YMapControls>
            
                        <YMapControls position="top right">
                          <YMapControl :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>
                          </YMapControl>
                        </YMapControls>
                      </YMap>`
                });
                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, YMapLocationRequest} from '@yandex/ymaps3-types';
import type {YMapDefaultMarkerProps} from '@yandex/ymaps3-default-ui-theme';

export const LOCATION: YMapLocationRequest = {
    center: [30.2931, 59.9299], // starting position [lng, lat]
    zoom: 14.7 // starting zoom
};

export const INITIAL_DRIVER_SPEED = 105;
export const MIN_DRIVER_SPEED = 10;
export const MAX_DRIVER_SPEED = 200;

export const ROUTE: {start: YMapDefaultMarkerProps; end: YMapDefaultMarkerProps} = {
    start: {
        zIndex: 1000,
        size: 'small',
        title: 'Парк',
        iconName: 'park',
        coordinates: [30.3114, 59.9241]
    },
    end: {
        zIndex: 1000,
        size: 'normal',
        title: 'Ледокол Красин',
        iconName: 'fallback',
        coordinates: [30.269, 59.9277]
    }
};

export const ROUTE_STYLE: DrawingStyle = {
    simplificationRate: 0,
    stroke: [
        {color: '#83C753', width: 7},
        {color: '#000000', opacity: 0.3, width: 9}
    ]
};

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

export const MARKER_IMAGE_PATH = '../marker-red.png';
import type {LineStringGeometry, LngLat, RouteFeature} from '@yandex/ymaps3-types';

// Wait for the api to load to access the map configuration
ymaps3.ready.then(() => {
    // 
    // Copy your api key for routes from the developer's dashboard and paste it here
    ymaps3.getDefaultConfig().setApikeys({router: '<YOUR_APIKEY>'});
    // 
    ymaps3.import.registerCdn('https://cdn.jsdelivr.net/npm/{package}', '@yandex/ymaps3-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 ymaps3.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;
}