Живые маркеры

<!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/turf.min.js"></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="../variables.ts"
        ></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">
            import type {LngLat, MapEventUpdateHandler, RouteFeature} from '@yandex/ymaps3-types';
            import {LOCATION, MARKERS} from '../variables';
            import {
                ZOOMS,
                angleFromCoordinate,
                animate,
                fetchRoute,
                MarkerSize,
                type MarkerProps,
                getAnimationDuration
            } from './common';
            
            window.map = null;
            
            main();
            
            async function main() {
                let markers = MARKERS.reduce<{[key: number]: MarkerProps}>((previousValue, currentValue) => {
                    previousValue[currentValue.id] = currentValue;
                    return previousValue;
                }, {});
                let size = MarkerSize.big;
            
                // Waiting for all api elements to be loaded
                await ymaps3.ready;
                const {YMap, YMapDefaultSchemeLayer, YMapDefaultFeaturesLayer, YMapMarker, YMapListener} = ymaps3;
            
                // 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 line
                        new YMapDefaultFeaturesLayer({})
                    ]
                );
            
                const previousCoordinates = new Map<number, LngLat>();
            
                const startAnimation = (markerProp: MarkerProps) => {
                    if (!previousCoordinates.has(markerProp.id)) {
                        previousCoordinates.set(markerProp.id, markerProp.coordinates);
                    }
            
                    const prevCoordinates = previousCoordinates.get(markerProp.id);
                    const nextCoordinates = markerProp.coordinates;
            
                    const diff: LngLat = [nextCoordinates[0] - prevCoordinates[0], nextCoordinates[1] - prevCoordinates[1]];
            
                    if (Math.abs(diff[0]) + Math.abs(diff[1]) < 1e-6) {
                        setNextCoordinates(markerProp);
                        return;
                    }
            
                    animate((progress) => {
                        markerProp.marker.update({
                            coordinates: [prevCoordinates[0] + diff[0] * progress, prevCoordinates[1] + diff[1] * progress]
                        });
                        if (progress === 1) {
                            previousCoordinates.set(markerProp.id, nextCoordinates);
                            setNextCoordinates(markerProp);
                        }
                    }, getAnimationDuration(prevCoordinates, nextCoordinates));
                };
            
                const setNextCoordinates = (markerProp: MarkerProps) => {
                    if (markerProp.route && markerProp.route.geometry.coordinates.length > 1) {
                        const coordinates = markerProp.route.geometry.coordinates.shift();
                        const angle = angleFromCoordinate(coordinates, markerProp.route.geometry.coordinates[0]);
                        markerProp.coordinates = coordinates;
            
                        const markerElement = document.getElementById(String(markerProp.id));
                        (markerElement.firstElementChild as HTMLElement).style.transform = `rotate(${angle}deg)`;
                        markerProp.angle = angle;
            
                        startAnimation(markerProp);
                    }
                };
            
                /* A handler function that updates the route line
                     and shifts the map to the new route boundaries, if they are available. */
                const routeHandler = (newRoute: RouteFeature, id: number) => {
                    if (markers[id]) {
                        markers[id].route = newRoute;
                    }
                    setNextCoordinates(markers[id]);
                };
            
                Object.values(markers).forEach((marker) => {
                    fetchRoute(marker.start, marker.end).then((route) => routeHandler(route, marker.id));
                });
            
                // Create markers with a custom icon and add them to the map
                Object.values(markers).forEach((markerProp) => {
                    const markerElement = document.createElement('div');
                    markerElement.id = String(markerProp.id);
                    markerElement.classList.add('marker_container', MarkerSize.big);
            
                    const markerElementImg = document.createElement('img');
                    markerElementImg.src = markerProp.background[MarkerSize.big];
                    markerElementImg.alt = 'marker';
                    markerElementImg.classList.add('marker', MarkerSize.big);
                    markerElement.appendChild(markerElementImg);
            
                    const markerElementIconImg = document.createElement('img');
                    markerElementIconImg.src = markerProp.icon[MarkerSize.big];
                    markerElementIconImg.alt = 'icon';
                    markerElementIconImg.classList.add('marker_icon', MarkerSize.big);
                    markerElement.appendChild(markerElementIconImg);
            
                    const markerElementText = document.createElement('span');
                    markerElementText.innerText = markerProp.text;
                    markerElementText.classList.add('marker_text', MarkerSize.big);
                    markerElement.appendChild(markerElementText);
            
                    const marker = new YMapMarker(
                        {coordinates: markerProp.coordinates, disableRoundCoordinates: true, zIndex: markerProp.id},
                        markerElement
                    );
                    markers[markerProp.id].marker = marker;
                    map.addChild(marker);
                });
            
                const updateHandler: MapEventUpdateHandler = (o) => {
                    let newSize;
                    if (o.location.zoom <= ZOOMS.small) {
                        newSize = MarkerSize.small;
                    } else if (o.location.zoom <= ZOOMS.big && o.location.zoom >= ZOOMS.small) {
                        newSize = MarkerSize.medium;
                    } else if (o.location.zoom >= ZOOMS.big) {
                        newSize = MarkerSize.big;
                    }
                    if (newSize !== size) {
                        Object.values(markers).forEach((markerProp) => {
                            const markerElement = document.getElementById(String(markerProp.id));
                            markerElement.classList.replace(size, newSize);
            
                            (markerElement.firstElementChild as HTMLImageElement).src = markerProp.background[newSize];
                            (markerElement.firstElementChild as HTMLImageElement).classList.replace(size, newSize);
            
                            markerElement.children[1].classList.replace(size, newSize);
                            (markerElement.children[1] as HTMLImageElement).src = markerProp.icon[newSize];
            
                            (markerElement.lastElementChild as HTMLSpanElement).innerText = markerProp.text;
                            (markerElement.lastElementChild as HTMLSpanElement).classList.replace(size, newSize);
                        });
                        size = newSize;
                    }
                };
            
                /* Add a listener to the map and pass the handlers functions for the events you want to process
              These are just some of the events, you can see them all in the documentation */
                map.addChild(
                    new YMapListener({
                        onUpdate: updateHandler
                    })
                );
            }
        </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" />
        <link rel="stylesheet" href="../variables.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/turf.min.js"></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="../variables.ts"
        ></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="react, typescript" type="text/babel">
            import type TReact from 'react';
            import type {LngLat, MapEventUpdateHandler, RouteFeature} from '@yandex/ymaps3-types';
            import {LOCATION, MARKERS} from '../variables';
            import {
                ZOOMS,
                angleFromCoordinate,
                animate,
                fetchRoute,
                MarkerSize,
                type MarkerProps,
                getAnimationDuration
            } from './common';
            
            window.map = null;
            
            main();
            
            type MarkerComponentProps = {
                marker: MarkerProps;
                size: MarkerSize;
                animationCb: (marker: MarkerProps) => void;
                zIndex: number;
            };
            
            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, YMapListener} = reactify.module(ymaps3);
                const {useEffect, useState, useCallback, startTransition, useRef} = React;
            
                const MarkerComponent: TReact.FC<MarkerComponentProps> = ({marker, size, animationCb, zIndex}) => {
                    const [coordinates, setCoordinates] = useState<LngLat>(() => marker.coordinates);
                    const prevCoordinatesRef = useRef<LngLat>(marker.coordinates);
            
                    useEffect(() => {
                        const prevCoordinates = prevCoordinatesRef.current;
                        const nextCoordinates = marker.coordinates;
                        const diff: LngLat = [nextCoordinates[0] - prevCoordinates[0], nextCoordinates[1] - prevCoordinates[1]];
                        prevCoordinatesRef.current = nextCoordinates;
            
                        if (Math.abs(diff[0]) + Math.abs(diff[1]) < 1e-6) {
                            animationCb(marker);
                            return;
                        }
            
                        animate((progress) => {
                            startTransition(() => {
                                setCoordinates([prevCoordinates[0] + diff[0] * progress, prevCoordinates[1] + diff[1] * progress]);
                            });
                            if (progress === 1) {
                                animationCb(marker);
                            }
                        }, getAnimationDuration(prevCoordinates, nextCoordinates));
                    }, [marker.coordinates]);
            
                    return (
                        <YMapMarker disableRoundCoordinates coordinates={coordinates} zIndex={zIndex}>
                            <div className={`marker_container ${size}`}>
                                <img
                                    src={marker.background[size]}
                                    alt="marker"
                                    style={{transform: `rotate(${marker.angle}deg)`}}
                                    className={`marker ${size}`}
                                />
                                {size !== MarkerSize.small && (
                                    <img src={marker.icon[size]} alt="icon" className={`marker_icon ${size}`} />
                                )}
                                {size === MarkerSize.big && <span className="marker_text">{marker.text}</span>}
                            </div>
                        </YMapMarker>
                    );
                };
            
                function App() {
                    const [size, setSize] = useState<MarkerSize>(MarkerSize.big);
                    const [markers, setMarkers] = useState<Array<MarkerProps>>(MARKERS);
            
                    const onUpdate: MapEventUpdateHandler = useCallback((o) => {
                        if (o.location.zoom <= ZOOMS.small) {
                            setSize(MarkerSize.small);
                        } else if (o.location.zoom <= ZOOMS.big && o.location.zoom >= ZOOMS.small) {
                            setSize(MarkerSize.medium);
                        } else if (o.location.zoom >= ZOOMS.big) {
                            setSize(MarkerSize.big);
                        }
                    }, []);
            
                    // Get and process route data during the first rendering
                    useEffect(() => {
                        markers.forEach((marker) => {
                            fetchRoute(marker.start, marker.end).then((route) => routeHandler(route, marker.id));
                        });
                    }, []);
            
                    /* A handler function that updates the route line
                       and shifts the map to the new route boundaries, if they are available. */
                    const routeHandler = (newRoute: RouteFeature, id: number) => {
                        setMarkers((state) =>
                            state.map((item) => {
                                if (item.id === id) {
                                    item.route = newRoute;
                                }
                                return item;
                            })
                        );
                        const marker = markers.find((item) => item.id === id);
                        setNextCoordinates(marker);
                    };
            
                    const setNextCoordinates = (markerProp: MarkerProps) => {
                        if (markerProp.route && markerProp.route.geometry.coordinates.length > 1) {
                            const coordinates = markerProp.route.geometry.coordinates.shift();
                            const angle = angleFromCoordinate(coordinates, markerProp.route.geometry.coordinates[0]);
                            setMarkers((state) =>
                                state.map((item) => {
                                    if (item.id === markerProp.id) {
                                        return {
                                            ...item,
                                            coordinates,
                                            angle
                                        };
                                    }
                                    return item;
                                })
                            );
                        }
                    };
            
                    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 line */}
                            <YMapDefaultFeaturesLayer />
            
                            {markers.map((marker) => (
                                <MarkerComponent
                                    key={marker.id}
                                    zIndex={marker.id}
                                    marker={marker}
                                    size={size}
                                    animationCb={() => setNextCoordinates(marker)}
                                />
                            ))}
            
                            <YMapListener onUpdate={onUpdate} />
                        </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" />
        <link rel="stylesheet" href="../variables.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/turf.min.js"></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="../variables.ts"
        ></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">
            import type {MapEventUpdateHandler, RouteFeature} from '@yandex/ymaps3-types';
            import {MARKERS, LOCATION} from '../variables';
            import {
                getAnimationDuration,
                ZOOMS,
                angleFromCoordinate,
                animate,
                fetchRoute,
                MarkerSize,
                type MarkerProps
            } from './common';
            
            window.map = null;
            
            async function main() {
                const [ymaps3Vue] = await Promise.all([ymaps3.import('@yandex/ymaps3-vuefy'), ymaps3.ready]);
                const vuefy = ymaps3Vue.vuefy.bindTo(Vue);
                const {YMap, YMapDefaultSchemeLayer, YMapDefaultFeaturesLayer, YMapMarker, YMapListener} = vuefy.module(ymaps3);
            
                const MarkerComponent = Vue.defineComponent({
                    components: {
                        YMapMarker
                    },
                    props: {
                        marker: {type: Object},
                        animationCb: {type: Function},
                        size: String,
                        zIndex: Number
                    },
                    setup(props) {
                        const coordinates = Vue.ref(props.marker.coordinates.slice());
                        const prevCoordinatesRef = Vue.ref(props.marker.coordinates.slice());
                        const angle = Vue.ref(props.marker.angle);
            
                        Vue.watch(
                            () => props.marker.coordinates,
                            (nextCoordinates) => {
                                const prevCoordinates = prevCoordinatesRef.value;
                                const diff = [nextCoordinates[0] - prevCoordinates[0], nextCoordinates[1] - prevCoordinates[1]];
                                prevCoordinatesRef.value = nextCoordinates;
            
                                if (Math.abs(diff[0]) + Math.abs(diff[1]) < 1e-6) {
                                    props.animationCb(props.marker);
                                    return;
                                }
            
                                animate((progress) => {
                                    coordinates.value = [
                                        prevCoordinates[0] + diff[0] * progress,
                                        prevCoordinates[1] + diff[1] * progress
                                    ];
                                    if (progress === 1) {
                                        props.animationCb(props.marker);
                                    }
                                }, getAnimationDuration(prevCoordinates, nextCoordinates));
                            }
                        );
            
                        Vue.watch(
                            () => props.marker.angle,
                            (angleProp) => {
                                angle.value = angleProp;
                            }
                        );
            
                        Vue.onMounted(() => {
                            prevCoordinatesRef.value = props.marker.coordinates.slice();
                        });
            
                        return {
                            background: props.marker.background,
                            icon: props.marker.icon,
                            text: props.marker.text,
                            angle,
                            coordinates,
                            zIndex: props.marker.id,
                            MarkerSize
                        };
                    },
                    template: `
                  <YMapMarker disableRoundCoordinates :coordinates="coordinates" :zIndex="zIndex">
                    <div :class="['marker_container', size]">
                      <img
                        :src="background[size]"
                        alt="marker"
                        :class="['marker', size]"
                        :style="{ transform: 'rotate(' + angle + 'deg)' }"
                      />
                      <img
                        v-if="size !== MarkerSize.small"
                        :src="icon[size]"
                        alt="icon"
                        :class="['marker_icon', size]"
                      />
                      <span v-if="size === MarkerSize.big" class="marker_text">{{ text }}</span>
                    </div>
                  </YMapMarker>
                `
                });
            
                const App = Vue.createApp({
                    components: {
                        YMap,
                        YMapDefaultSchemeLayer,
                        YMapDefaultFeaturesLayer,
                        YMapListener,
                        MarkerComponent
                    },
                    setup() {
                        let markers = Vue.ref(MARKERS);
                        const size = Vue.ref(MarkerSize.big);
            
                        const refMap = (ref) => {
                            window.map = ref?.entity;
                        };
            
                        const onUpdate: MapEventUpdateHandler = (o) => {
                            if (o.location.zoom <= ZOOMS.small) {
                                size.value = MarkerSize.small;
                            } else if (o.location.zoom <= ZOOMS.big && o.location.zoom >= ZOOMS.small) {
                                size.value = MarkerSize.medium;
                            } else if (o.location.zoom >= ZOOMS.big) {
                                size.value = MarkerSize.big;
                            }
                        };
            
                        const setNextCoordinates = (markerProp: MarkerProps) => {
                            if (markerProp.route && markerProp.route.geometry.coordinates.length > 1) {
                                const coordinates = markerProp.route.geometry.coordinates.shift();
                                const angle = angleFromCoordinate(coordinates, markerProp.route.geometry.coordinates[0]);
                                markers.value = markers.value.map((marker) => {
                                    if (marker.id === markerProp.id) {
                                        return {
                                            ...marker,
                                            coordinates,
                                            angle
                                        };
                                    }
                                    return marker;
                                });
                            }
                        };
                        /* A handler function that updates the route line
                         and shifts the map to the new route boundaries, if they are available. */
                        const routeHandler = (newRoute: RouteFeature, id: number) => {
                            markers.value = markers.value.map((marker) => {
                                if (marker.id === id) {
                                    return {
                                        ...marker,
                                        route: newRoute
                                    };
                                }
                                return marker;
                            });
                            const marker = markers.value.find((marker) => marker.id === id);
                            setNextCoordinates(marker);
                        };
            
                        Vue.onMounted(() => {
                            markers.value.forEach((marker) => {
                                fetchRoute(marker.start, marker.end).then((route) => routeHandler(route, marker.id));
                            });
                        });
            
                        const callback = (marker: MarkerProps) => {
                            setNextCoordinates(marker);
                        };
            
                        return {
                            refMap,
                            LOCATION,
                            markers,
                            onUpdate,
                            size,
                            callback
                        };
                    },
                    template: `
                  <YMap
                    :location="LOCATION"
                    :ref="refMap"
                    :showScaleInCopyrights="true"
                  >
                    <YMapDefaultSchemeLayer />
                    <YMapDefaultFeaturesLayer />
            
                    <template v-for="marker in markers" :key="marker.id">
                      <MarkerComponent :marker="marker" :size="size" :animationCb="callback" />
                    </template>
            
                    <YMapListener :onUpdate="onUpdate" />
                  </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" />
        <link rel="stylesheet" href="../variables.css" />
    </head>
    <body>
        <div id="app"></div>
    </body>
</html>
import type {YMapLocationRequest} from '@yandex/ymaps3-types';
import {MarkerProps} from './common';

export const LOCATION: YMapLocationRequest = {
    center: [37.629, 55.7521], // starting position [lng, lat]
    zoom: 13 // starting zoom
};

const MARKER_ICONS = {
    small: '../bus-medium.svg',
    medium: '../bus-medium.svg',
    big: '../bus-big.svg'
};
const MARKER_BACKGROUNDS = {
    small: '../marker-small.svg',
    medium: '../marker-medium.svg',
    big: '../marker-big.svg'
};

export const MARKERS: Array<MarkerProps> = [
    {
        angle: 0,
        coordinates: [37.620028, 55.741556],
        start: [37.620028, 55.741556],
        end: [38.130492, 56.31112],
        route: null,
        id: 1,
        text: 'M104',
        icon: MARKER_ICONS,
        background: MARKER_BACKGROUNDS
    },
    {
        angle: 0,
        coordinates: [37.5698, 55.7297],
        start: [37.5698, 55.7297],
        end: [37.6691, 55.7263],
        route: null,
        id: 2,
        text: 'M106',
        icon: MARKER_ICONS,
        background: MARKER_BACKGROUNDS
    },
    {
        angle: 0,
        coordinates: [37.5871, 55.7822],
        start: [37.5871, 55.7822],
        end: [37.6814, 55.7585],
        route: null,
        id: 3,
        text: 'M110',
        icon: MARKER_ICONS,
        background: MARKER_BACKGROUNDS
    },
    {
        angle: 0,
        coordinates: [37.5898, 55.7705],
        start: [37.5898, 55.7705],
        end: [37.6808, 55.7524],
        route: null,
        id: 4,
        text: 'M111',
        icon: MARKER_ICONS,
        background: MARKER_BACKGROUNDS
    },
    {
        angle: 0,
        coordinates: [37.6483, 55.7547],
        start: [37.6483, 55.7547],
        end: [37.6322, 55.729],
        route: null,
        id: 5,
        text: 'M120',
        icon: MARKER_ICONS,
        background: MARKER_BACKGROUNDS
    },
    {
        angle: 0,
        coordinates: [37.5822, 55.7476],
        start: [37.58222, 55.7476],
        end: [37.63105, 55.7665],
        route: null,
        id: 6,
        text: 'M121',
        icon: MARKER_ICONS,
        background: MARKER_BACKGROUNDS
    },
    {
        angle: 0,
        coordinates: [37.6423, 55.7421],
        start: [37.6423, 55.74219],
        end: [37.6084, 55.7393],
        route: null,
        id: 7,
        text: 'M122',
        icon: MARKER_ICONS,
        background: MARKER_BACKGROUNDS
    },
    {
        angle: 0,
        coordinates: [37.5975, 55.7576],
        start: [37.5975, 55.7576],
        end: [37.6459, 55.761],
        route: null,
        id: 8,
        text: 'M123',
        icon: MARKER_ICONS,
        background: MARKER_BACKGROUNDS
    },
    {
        angle: 0,
        coordinates: [37.5828, 55.7478],
        start: [37.5828, 55.7478],
        end: [37.6274, 55.7737],
        route: null,
        id: 9,
        text: 'M124',
        icon: MARKER_ICONS,
        background: MARKER_BACKGROUNDS
    },
    {
        angle: 0,
        coordinates: [37.6088, 55.7496],
        start: [37.6088, 55.7496],
        end: [37.6383, 55.7649],
        route: null,
        id: 10,
        text: 'M125',
        icon: MARKER_ICONS,
        background: MARKER_BACKGROUNDS
    },
    {
        angle: 0,
        coordinates: [37.6715, 55.7364],
        start: [37.6715, 55.7364],
        end: [37.64402, 55.7482],
        route: null,
        id: 11,
        text: 'M126',
        icon: MARKER_ICONS,
        background: MARKER_BACKGROUNDS
    }
];
:root {
    --color-text: #3cb200;
    --left-medium-marker: 8px;
    --bottom-medium-marker: 7px;
    --left-big-marker: 8px;
    --bottom-big-marker: 8px;
}
import {LngLat, RouteFeature, YMapMarker} from '@yandex/ymaps3-types';

export enum MarkerSize {
    small = 'small',
    big = 'big',
    medium = 'medium'
}

export type MarkerProps = {
    angle: number;
    start: LngLat;
    end: LngLat;
    route: RouteFeature;
    coordinates: LngLat;
    id: number;
    text: string;
    marker?: YMapMarker;
    icon: {
        [key in MarkerSize]: string;
    };
    background: {
        [key in MarkerSize]: string;
    };
};

// 
// 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>'});
});
// 

export const ZOOMS = {
    small: 12,
    big: 13
};

export const MARKER_SPEED = 20; // m/s

export function animate(cb: (progress: number) => void, duration: number) {
    const startTime = Date.now();
    function tick() {
        const progress = (Date.now() - startTime) / duration;
        if (progress >= 1) {
            cb(1);
            return;
        }

        cb(progress);
        requestAnimationFrame(tick);
    }

    requestAnimationFrame(tick);
}

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 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 brng = Math.atan2(y, x);

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

    return brng;
}

export function getAnimationDuration(prevCoordinates: LngLat, nextCoordinates: LngLat): number {
    const pathLength = turf.length(turf.lineString([prevCoordinates, nextCoordinates]), {units: 'meters'});
    return (pathLength / MARKER_SPEED) * 1000; // in ms
}
.marker_container {
    position: absolute;
    transform: translate(-50%, -50%);
}

.marker_container.big {
    width: 38px;
    height: 38px;
}

.marker_container.medium {
    width: 28px;
    height: 28px;
}

.marker_text {
    position: absolute;
    background: white;
    left: 18px;
    bottom: 9px;
    z-index: -1;
    border-radius: 8px;
    padding: 4px 4px 4px 16px;
    font-size: 14px;
    line-height: 16px;
    color: var(--color-text);
    font-weight: 500;
}

.marker.big {
    transform-origin: 50% 57%;
    width: 38px;
    height: 38px;
}
.marker.medium {
    transform-origin: 50% 55%;
    width: 28px;
    height: 28px;
}

.marker_text.big {
    display: inline-block;
}

.marker_text.medium {
    display: none;
}

.marker_text.small {
    display: none;
}

.marker_icon.big {
    position: absolute;
    left: 11px;
    bottom: 9px;
}

.marker_icon.small {
    display: none;
}

.marker_icon.medium {
    position: absolute;
    left: var(--left-medium-marker);
    bottom: var(--bottom-medium-marker);
}