Калькулятор стоимости доставки

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/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 {
                LOCATION,
                ROUTE_START,
                OUT_OF_ZONES_PRICE,
                ROUTE_STYLES,
                TRANSLATIONS,
                ZONES,
                END_MARKER_COLOR,
                START_MARKER_COLOR
            } from '../variables';
            import type {DomEventHandler, RouteFeature} from '@yandex/ymaps3-types';
            import {calculatePrice, fetchRoute, type MapZone} from './common';
            
            window.map = null;
            
            interface InfoMessageProps {
                text: string;
            }
            
            interface DeliverySumControlProps {
                currentZone: MapZone;
                outOfZoneLineLength: number;
                price: number;
            }
            
            main();
            
            async function main() {
                // Waiting for all api elements to be loaded
                await ymaps3.ready;
                const {
                    YMap,
                    YMapDefaultSchemeLayer,
                    YMapDefaultFeaturesLayer,
                    YMapFeature,
                    YMapListener,
                    YMapControls,
                    YMapControl
                } = ymaps3;
                const {YMapDefaultMarker, YMapSearchControl} = await ymaps3.import('@yandex/ymaps3-default-ui-theme');
            
                class InfoMessageClass extends ymaps3.YMapComplexEntity<InfoMessageProps> {
                    private _element!: HTMLDivElement;
                    private _detachDom!: () => void;
            
                    // Method for create a DOM control element
                    _createElement(props: InfoMessageProps) {
                        // Create a root element
                        const infoWindow = document.createElement('div');
                        infoWindow.classList.add('info-window');
                        infoWindow.innerHTML = props.text;
            
                        return infoWindow;
                    }
            
                    // Method for attaching the control to the map
                    _onAttach() {
                        this._element = this._createElement(this._props);
                        this._detachDom = ymaps3.useDomContext(this, this._element, this._element);
                    }
            
                    // Method for detaching control from the map
                    _onDetach() {
                        this._detachDom();
                        this._detachDom = undefined;
                        this._element = undefined;
                    }
                }
            
                class DeliveryCostControl extends ymaps3.YMapComplexEntity<{}> {
                    private _element!: HTMLDivElement;
                    private _detachDom!: () => void;
            
                    // Method for create a DOM control element
                    _createElement() {
                        // Create a root element
                        const windowElement = document.createElement('div');
                        windowElement.classList.add('delivery-cost-window');
            
                        const windowTitle = document.createElement('div');
                        windowTitle.classList.add('delivery-cost-title');
                        windowTitle.innerText = TRANSLATIONS.deliveryWindowTitle;
            
                        const windowContent = document.createElement('div');
                        windowContent.classList.add('delivery-cost-content');
            
                        for (const zone of ZONES) {
                            const zoneItem = document.createElement('div');
                            zoneItem.classList.add('delivery-item');
            
                            const colorBox = document.createElement('div');
                            colorBox.classList.add('delivery-item-colorbox');
                            colorBox.style.backgroundColor = zone.style.fill;
                            colorBox.style.borderColor = zone.style.stroke[0].color;
            
                            const text = document.createElement('div');
                            text.innerText = `${zone.name}${zone.price} ${TRANSLATIONS.currency}`;
                            zoneItem.appendChild(colorBox);
                            zoneItem.appendChild(text);
                            windowContent.appendChild(zoneItem);
                        }
                        const divider = document.createElement('hr');
                        divider.classList.add('divider');
            
                        const windowFooter = document.createElement('div');
                        windowFooter.classList.add('delivery-cost-footer');
                        windowFooter.innerText = `${TRANSLATIONS.deliveryWindowFooter} ${OUT_OF_ZONES_PRICE} ${TRANSLATIONS.currency}`;
            
                        windowElement.appendChild(windowTitle);
                        windowElement.appendChild(windowContent);
                        windowElement.appendChild(divider);
                        windowElement.appendChild(windowFooter);
            
                        return windowElement;
                    }
            
                    // Method for attaching the control to the map
                    _onAttach() {
                        this._element = this._createElement();
                        this._detachDom = ymaps3.useDomContext(this, this._element, this._element);
                    }
            
                    // Method for detaching control from the map
                    _onDetach() {
                        this._detachDom();
                        this._detachDom = undefined;
                        this._element = undefined;
                    }
                }
            
                class DeliverySumControl extends ymaps3.YMapComplexEntity<{}> {
                    private _element!: HTMLDivElement;
                    private _detachDom!: () => void;
                    private contentElement: HTMLDivElement;
                    private footerElement: HTMLDivElement;
            
                    // Method for create a DOM control element
                    _createElement() {
                        // Create a root element
                        const windowElement = document.createElement('div');
                        windowElement.classList.add('delivery-sum-window');
            
                        const windowTitle = document.createElement('div');
                        windowTitle.classList.add('delivery-sum-title');
                        windowTitle.innerText = TRANSLATIONS.deliverySumTitle;
            
                        const windowContent = document.createElement('div');
                        windowContent.classList.add('delivery-sum-content');
                        windowContent.id = 'delivery-sum-content';
            
                        windowElement.appendChild(windowTitle);
                        windowElement.appendChild(windowContent);
            
                        const windowFooter = document.createElement('div');
                        windowFooter.classList.add('delivery-sum-footer');
                        windowFooter.id = 'delivery-sum-footer';
                        windowElement.appendChild(windowFooter);
            
                        this.contentElement = windowContent;
                        this.footerElement = windowFooter;
            
                        return windowElement;
                    }
            
                    update(changedProps: Partial<DeliverySumControlProps>) {
                        this.contentElement.innerText = `${
                            !changedProps.outOfZoneLineLength && changedProps.currentZone ? `${changedProps.currentZone.name}` : ''
                        } ${changedProps.price.toFixed()} ${TRANSLATIONS.currency}`;
            
                        if (changedProps.outOfZoneLineLength) {
                            this.footerElement.classList.remove('hidden');
                        } else {
                            this.footerElement.classList.add('hidden');
                        }
                        this.footerElement.innerText = `${changedProps.currentZone.name} ${
                            !!changedProps.outOfZoneLineLength
                                ? `+ ${changedProps.outOfZoneLineLength.toFixed()}${TRANSLATIONS.units}`
                                : ''
                        }`;
                    }
            
                    // Method for attaching the control to the map
                    _onAttach() {
                        this._element = this._createElement();
                        this._detachDom = ymaps3.useDomContext(this, this._element, this._element);
                    }
            
                    // Method for detaching control from the map
                    _onDetach() {
                        this._detachDom();
                        this._detachDom = undefined;
                        this._element = undefined;
                    }
                }
            
                // 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({}), new YMapDefaultFeaturesLayer({})]
                );
                const rerenderComponents = ({price, outOfZoneLineLength, currentZone, coordinates, routeGeometry}) => {
                    if (!route.parent) {
                        map.addChild(route);
                    }
                    route.update({geometry: routeGeometry});
            
                    if (!marker.parent) {
                        map.addChild(marker);
                    }
                    marker.update({coordinates});
            
                    if (!deliverySumControl.parent) {
                        leftControl.addChild(deliverySumControl);
                    }
                    deliverySumControl.update({currentZone, price, outOfZoneLineLength});
                };
            
                /* 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) => {
                    const props = calculatePrice(newRoute);
                    return {
                        ...props,
                        routeGeometry: newRoute.geometry
                    };
                };
            
                const searchHandler = (searchResults) => {
                    fetchRoute(ROUTE_START, searchResults[0].geometry.coordinates).then((route) => {
                        const renderProps = routeHandler(route);
                        rerenderComponents({...renderProps, coordinates: searchResults[0].geometry.coordinates});
                    });
                };
            
                const onMapClick: DomEventHandler = (object, event) => {
                    fetchRoute(ROUTE_START, event.coordinates).then((route) => {
                        const renderProps = routeHandler(route);
                        rerenderComponents({...renderProps, coordinates: event.coordinates});
                    });
                };
            
                const route = new YMapFeature({
                    geometry: {type: 'LineString', coordinates: []},
                    style: ROUTE_STYLES
                });
            
                const deliverySumControl = new DeliverySumControl({});
            
                const marker = new YMapDefaultMarker({
                    coordinates: ROUTE_START,
                    iconName: 'building',
                    size: 'normal',
                    color: {day: END_MARKER_COLOR, night: END_MARKER_COLOR}
                });
            
                ZONES.forEach((zone) => map.addChild(new YMapFeature(zone)));
            
                map.addChild(
                    new YMapDefaultMarker({
                        coordinates: ROUTE_START,
                        iconName: 'malls',
                        size: 'normal',
                        color: {day: START_MARKER_COLOR, night: START_MARKER_COLOR}
                    })
                );
            
                map.addChild(
                    new YMapControls({position: 'top right'}, [
                        new YMapSearchControl({
                            searchResult: searchHandler
                        })
                    ])
                );
            
                const leftControl = new YMapControls({position: 'top left', orientation: 'vertical'}, [
                    new YMapControl({transparent: true}).addChild(new InfoMessageClass({text: TRANSLATIONS.tooltip})),
                    new YMapControl({transparent: true}).addChild(new DeliveryCostControl({}))
                ]);
                map.addChild(leftControl);
            
                map.addChild(new YMapListener({onClick: onMapClick}));
            }
        </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@17/umd/react.production.min.js"></script>
        <script crossorigin src="https://cdn.jsdelivr.net/npm/react-dom@17/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="react, typescript"
            type="text/babel"
            src="../variables.ts"
        ></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">
            import {
                LOCATION,
                OUT_OF_ZONES_PRICE,
                ROUTE_START,
                ROUTE_STYLES,
                TRANSLATIONS,
                ZONES,
                END_MARKER_COLOR,
                START_MARKER_COLOR
            } from '../variables';
            import type {DomEventHandler, LngLat, RouteFeature, LineStringGeometry} from '@yandex/ymaps3-types';
            import {calculatePrice, fetchRoute, type MapZone} from './common';
            
            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,
                    YMapFeature,
                    YMapListener,
                    YMapControls,
                    YMapControl
                } = reactify.module(ymaps3);
                const {YMapDefaultMarker, YMapSearchControl} = reactify.module(
                    await ymaps3.import('@yandex/ymaps3-default-ui-theme')
                );
            
                const {useState, useCallback} = React;
            
                function App() {
                    const [price, setPrice] = useState(null);
                    const [currentZone, setCurrentZone] = useState<MapZone>(null);
                    const [outOfZoneLineLength, setOutOfZoneLineLength] = useState(null);
                    const [finishCoordinates, setFinishCoordinates] = useState<LngLat>(null);
                    const [routeGeometry, setRouteGeometry] = useState<LineStringGeometry>(null);
            
                    const onMapClick: DomEventHandler = useCallback((object, event) => {
                        setFinishCoordinates(event.coordinates);
                        fetchRoute(ROUTE_START, event.coordinates).then((route) => routeHandler(route));
                    }, []);
            
                    /* A handler function that updates the route line
                       and shifts the map to the new route boundaries, if they are available. */
                    const routeHandler = useCallback((newRoute: RouteFeature) => {
                        setRouteGeometry(newRoute.geometry);
                        const {outOfZoneLineLength, price, currentZone} = calculatePrice(newRoute);
                        setPrice(price);
                        setOutOfZoneLineLength(outOfZoneLineLength);
                        setCurrentZone(currentZone);
                    }, []);
            
                    const searchHandler = (searchResults) => {
                        setFinishCoordinates(searchResults[0].geometry.coordinates);
                        fetchRoute(ROUTE_START, searchResults[0].geometry.coordinates).then((route) => routeHandler(route));
                    };
            
                    return (
                        // Initialize the map and pass initialization parameters
                        <YMap location={LOCATION} showScaleInCopyrights={true} ref={(x) => (map = x)}>
                            {/* Add a map scheme layer */}
                            <YMapDefaultSchemeLayer />
                            <YMapDefaultFeaturesLayer />
            
                            {ZONES.map((zone) => (
                                <YMapFeature key={zone.name} style={zone.style} geometry={zone.geometry} />
                            ))}
            
                            {routeGeometry && <YMapFeature style={ROUTE_STYLES} geometry={routeGeometry} />}
            
                            <YMapDefaultMarker
                                color={{day: START_MARKER_COLOR, night: START_MARKER_COLOR}}
                                size="normal"
                                iconName="malls"
                                coordinates={ROUTE_START}
                            />
            
                            {finishCoordinates && (
                                <YMapDefaultMarker
                                    size="normal"
                                    color={{day: END_MARKER_COLOR, night: END_MARKER_COLOR}}
                                    iconName="building"
                                    coordinates={finishCoordinates}
                                />
                            )}
            
                            <YMapControls position="top right">
                                <YMapSearchControl searchResult={searchHandler} />
                            </YMapControls>
            
                            <YMapControls position="top left" orientation="vertical">
                                <YMapControl transparent>
                                    <div className="info-window">{TRANSLATIONS.tooltip}</div>
                                </YMapControl>
            
                                <YMapControl transparent>
                                    <div className="delivery-cost-window">
                                        <div className="delivery-cost-title">{TRANSLATIONS.deliveryWindowTitle}</div>
                                        <div className="delivery-cost-content">
                                            {ZONES.map((zone) => (
                                                <div key={zone.name} className="delivery-item">
                                                    <div
                                                        className="delivery-item-colorbox"
                                                        style={{
                                                            backgroundColor: zone.style.fill,
                                                            borderColor: zone.style.stroke[0].color
                                                        }}
                                                    />
                                                    <div>
                                                        {zone.name} — {zone.price} {TRANSLATIONS.currency}
                                                    </div>
                                                </div>
                                            ))}
                                        </div>
                                        <hr className="divider" />
                                        <div className="delivery-cost-footer">
                                            {TRANSLATIONS.deliveryWindowFooter} {OUT_OF_ZONES_PRICE} {TRANSLATIONS.currency}
                                        </div>
                                    </div>
                                </YMapControl>
            
                                {currentZone && price && (
                                    <YMapControl transparent>
                                        <div className="delivery-sum-window">
                                            <div className="delivery-sum-title">{TRANSLATIONS.deliverySumTitle}</div>
                                            <div className="delivery-sum-content">
                                                {!outOfZoneLineLength && currentZone ? `${currentZone.name}` : ''} {price.toFixed()}
                                                {TRANSLATIONS.currency}
                                            </div>
                                            {outOfZoneLineLength && (
                                                <div className="delivery-sum-footer">
                                                    {currentZone.name} + {outOfZoneLineLength.toFixed()}
                                                    {TRANSLATIONS.units}
                                                </div>
                                            )}
                                        </div>
                                    </YMapControl>
                                )}
                            </YMapControls>
            
                            <YMapListener onClick={onMapClick} />
                        </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/turf.min.js"></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="../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 {
                LOCATION,
                ROUTE_START,
                OUT_OF_ZONES_PRICE,
                ROUTE_STYLES,
                TRANSLATIONS,
                ZONES,
                END_MARKER_COLOR,
                START_MARKER_COLOR
            } from '../variables';
            import type {DomEventHandler, RouteFeature} from '@yandex/ymaps3-types';
            import {calculatePrice, fetchRoute} from './common';
            
            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,
                    YMapListener,
                    YMapControls,
                    YMapControl
                } = vuefy.module(ymaps3);
                const {YMapDefaultMarker, YMapSearchControl} = vuefy.module(await ymaps3.import('@yandex/ymaps3-default-ui-theme'));
            
                const app = Vue.createApp({
                    components: {
                        YMap,
                        YMapDefaultSchemeLayer,
                        YMapDefaultFeaturesLayer,
                        YMapFeature,
                        YMapDefaultMarker,
                        YMapControls,
                        YMapControl,
                        YMapListener,
                        YMapSearchControl
                    },
                    setup() {
                        const refMap = (ref) => {
                            window.map = ref?.entity;
                        };
                        const routeGeometry = Vue.ref(null);
                        const finishCoordinates = Vue.ref(null);
                        const price = Vue.ref(null);
                        const outOfZoneLineLength = Vue.ref(null);
                        const currentZone = Vue.ref(null);
            
                        const priceFixed = Vue.computed(() => price.toFixed());
                        const outOfZoneLineLengthFixed = Vue.computed(() => outOfZoneLineLength.toFixed());
            
                        /* 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) => {
                            routeGeometry.value = newRoute.geometry;
                            const {
                                outOfZoneLineLength: newOutOfZoneLineLength,
                                price: newPrice,
                                currentZone: newCurrentZone
                            } = calculatePrice(newRoute);
                            price.value = newPrice;
                            outOfZoneLineLength.value = newOutOfZoneLineLength;
                            currentZone.value = newCurrentZone;
                        };
            
                        const searchHandler = (searchResults) => {
                            finishCoordinates.value = searchResults[0].geometry.coordinates;
                            fetchRoute(ROUTE_START, searchResults[0].geometry.coordinates).then((route) => routeHandler(route));
                        };
            
                        const onMapClick: DomEventHandler = (object, event) => {
                            finishCoordinates.value = event.coordinates;
                            fetchRoute(ROUTE_START, event.coordinates).then((route) => routeHandler(route));
                        };
            
                        return {
                            LOCATION,
                            ROUTE_START,
                            ZONES,
                            ROUTE_STYLES,
                            TRANSLATIONS,
                            OUT_OF_ZONES_PRICE,
                            END_MARKER_COLOR,
                            START_MARKER_COLOR,
                            refMap,
                            routeGeometry,
                            finishCoordinates,
                            currentZone,
                            price,
                            priceFixed,
                            outOfZoneLineLength,
                            outOfZoneLineLengthFixed,
                            searchHandler,
                            onMapClick
                        };
                    },
                    template: `
                  <!-- Initialize the map and pass initialization parameters -->
                  <YMap
                    :location="LOCATION"
                    :showScaleInCopyrights="true"
                    :ref="refMap"
                  >
                    <!-- Add a map scheme layer -->
                    <YMapDefaultSchemeLayer/>
                    <YMapDefaultFeaturesLayer/>
            
                    <template v-for="zone in ZONES" :key="zone.name">
                      <YMapFeature  :geometry="zone.geometry" :style="zone.style" />
                    </template>
            
                    <YMapFeature v-if="routeGeometry" :style="ROUTE_STYLES" :geometry="routeGeometry"/>
            
                    <YMapDefaultMarker
                      size="normal"
                      iconName="malls"
                      :coordinates="ROUTE_START"
                      :color="{day: START_MARKER_COLOR, night: START_MARKER_COLOR}"
                    />
            
                    <YMapDefaultMarker
                      v-if="finishCoordinates"
                      size="normal"
                      iconName="building"
                      :coordinates="finishCoordinates"
                      :color="{day: END_MARKER_COLOR, night: END_MARKER_COLOR}"
                    />
            
                    <YMapControls position="top right">
                      <YMapSearchControl :searchResult="searchHandler" />
                    </YMapControls>
            
                    <YMapControls position="top left" orientation="vertical">
                      <YMapControl :transparent="true">
                        <div class="info-window">
                          {{ TRANSLATIONS.tooltip }}
                        </div>
                      </YMapControl>
            
                      <YMapControl :transparent="true">
                        <div class="delivery-cost-window">
                          <div class="delivery-cost-title">
                            {{ TRANSLATIONS.deliveryWindowTitle }}
                          </div>
                          <div class="delivery-cost-content">
                            <template v-for="zone in ZONES" :key="zone.name">
                              <div class="delivery-item">
                                <div class="delivery-item-colorbox" :style="{ backgroundColor: zone.style.fill, borderColor: zone.style.stroke[0].color }"/>
                                <div>
                                  {{ zone.name }}{{ zone.price }} {{ TRANSLATIONS.currency }}
                                </div>
                              </div>
                            </template>
                          </div>
                          <hr class="divider"/>
                          <div class="delivery-cost-footer">
                            {{ TRANSLATIONS.deliveryWindowFooter }} {{ OUT_OF_ZONES_PRICE }} {{ TRANSLATIONS.currency }}
                          </div>
                        </div>
                      </YMapControl>
            
                      <YMapControl :transparent="true" v-if="currentZone && price">
                        <div class="delivery-sum-window">
                          <div class="delivery-sum-title">
                            {{ TRANSLATIONS.deliverySumTitle }}
                          </div>
                          <div class="delivery-sum-content">
                            {{ !outOfZoneLineLength && currentZone ? currentZone.name : '' }} {{ priceFixed }} {{ TRANSLATIONS.currency }}
                          </div>
                          <div class="delivery-sum-footer" v-if="outOfZoneLineLength">
                            {{ currentZone.name }} + {{ outOfZoneLineLengthFixed }} {{ TRANSLATIONS.units }}
                          </div>
                        </div>
                      </YMapControl>
                    </YMapControls>
            
                    <YMapListener @click="onMapClick" />
                  </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, LngLat, YMapLocationRequest} from '@yandex/ymaps3-types';
import {MapZone} from './common';

export const LOCATION: YMapLocationRequest = {
    center: [37.6225, 55.7536], // starting position [lng, lat]
    zoom: 11.2 // starting zoom
};

export const ROUTE_START: LngLat = [37.6225, 55.7536];

export const END_MARKER_COLOR = '#313133';
export const START_MARKER_COLOR = '#EB5547';

export const ROUTE_STYLES: DrawingStyle = {
    simplificationRate: 0,
    stroke: [
        {color: '#83C753', width: 6},
        {color: '#000000', width: 8, opacity: 0.3}
    ],
    fill: '#83C753'
};

export const ZONES: Array<MapZone> = [
    {
        style: {
            simplificationRate: 0,
            stroke: [{color: '#EF9A7A', width: 3}],
            fill: 'rgba(239, 154, 122, 0.29)'
        },
        geometry: {
            type: 'Polygon',
            coordinates: [
                [
                    [37.6329, 55.7727],
                    [37.6371, 55.7723],
                    [37.6496, 55.7688],
                    [37.6562, 55.7644],
                    [37.6578, 55.7613],
                    [37.6573, 55.7549],
                    [37.656, 55.7526],
                    [37.6549, 55.7427],
                    [37.6523, 55.7405],
                    [37.6417, 55.7332],
                    [37.6376, 55.7313],
                    [37.6237, 55.7296],
                    [37.6122, 55.7298],
                    [37.586, 55.7384],
                    [37.5826, 55.7466],
                    [37.583, 55.7524],
                    [37.5848, 55.7589],
                    [37.5887, 55.7642],
                    [37.5947, 55.7687],
                    [37.6007, 55.7714],
                    [37.6065, 55.7729],
                    [37.6175, 55.7737],
                    [37.6267, 55.7735],
                    [37.6329, 55.7727]
                ]
            ]
        },
        price: 300,
        priority: 1,
        name: 'зона A'
    },
    {
        style: {
            simplificationRate: 0,
            stroke: [{color: '#EE5441', width: 3}],
            fill: 'rgba(238, 84, 65, 0.1)'
        },
        geometry: {
            type: 'Polygon',
            coordinates: [
                [
                    [37.5839, 55.7093],
                    [37.587, 55.7072],
                    [37.6093, 55.7009],
                    [37.6141, 55.7017],
                    [37.6187, 55.7049],
                    [37.6224, 55.7059],
                    [37.6566, 55.7031],
                    [37.6725, 55.7117],
                    [37.7111, 55.7232],
                    [37.7115, 55.7256],
                    [37.6992, 55.7362],
                    [37.6971, 55.7401],
                    [37.6984, 55.7438],
                    [37.6999, 55.7474],
                    [37.6923, 55.7555],
                    [37.6863, 55.7581],
                    [37.685, 55.7597],
                    [37.6847, 55.7615],
                    [37.6848, 55.7633],
                    [37.6888, 55.7693],
                    [37.6881, 55.7713],
                    [37.6818, 55.7765],
                    [37.67, 55.7812],
                    [37.6632, 55.7854],
                    [37.6524, 55.7934],
                    [37.6471, 55.794],
                    [37.6353, 55.7921],
                    [37.6292, 55.7921],
                    [37.6175, 55.7933],
                    [37.5744, 55.7916],
                    [37.5657, 55.7854],
                    [37.5579, 55.78],
                    [37.554, 55.7753],
                    [37.5522, 55.7744],
                    [37.5461, 55.7734],
                    [37.5423, 55.7704],
                    [37.5377, 55.7666],
                    [37.5343, 55.759],
                    [37.5309, 55.7516],
                    [37.5353, 55.7404],
                    [37.5423, 55.7342],
                    [37.5473, 55.7278],
                    [37.5503, 55.7243],
                    [37.5574, 55.7213],
                    [37.5693, 55.7177],
                    [37.576, 55.715],
                    [37.5839, 55.7093]
                ]
            ]
        },
        price: 500,
        priority: 2,
        name: 'зона B'
    },
    {
        style: {
            simplificationRate: 0,
            stroke: [{color: '#709FD3', width: 3}],
            fill: 'rgba(112, 159, 211, 0.1)'
        },
        geometry: {
            type: 'Polygon',
            coordinates: [
                [
                    [37.8385, 55.6577],
                    [37.8298, 55.6919],
                    [37.8353, 55.707],
                    [37.8393, 55.7146],
                    [37.8447, 55.7776],
                    [37.8399, 55.8126],
                    [37.8376, 55.8241],
                    [37.7288, 55.8812],
                    [37.7067, 55.8914],
                    [37.6933, 55.895],
                    [37.6523, 55.8958],
                    [37.5931, 55.9077],
                    [37.5766, 55.9113],
                    [37.5387, 55.9073],
                    [37.5222, 55.9037],
                    [37.478, 55.886],
                    [37.4662, 55.883],
                    [37.4473, 55.8825],
                    [37.4134, 55.8719],
                    [37.4008, 55.8631],
                    [37.3929, 55.8467],
                    [37.3952, 55.8344],
                    [37.3858, 55.8038],
                    [37.3716, 55.7891],
                    [37.3693, 55.7488],
                    [37.3858, 55.7137],
                    [37.4118, 55.6902],
                    [37.426, 55.6693],
                    [37.4583, 55.6391],
                    [37.4961, 55.6088],
                    [37.5103, 55.5959],
                    [37.5994, 55.5753],
                    [37.6691, 55.5719],
                    [37.6797, 55.5731],
                    [37.7431, 55.5972],
                    [37.7881, 55.6204],
                    [37.8267, 55.6451],
                    [37.8385, 55.6577]
                ]
            ]
        },
        price: 900,
        priority: 3,
        name: 'зона C'
    }
];

export const OUT_OF_ZONES_PRICE = 200;

export const TRANSLATIONS = {
    deliveryWindowTitle: 'Стоимость доставки',
    deliveryWindowFooter: 'Каждый доп. 1км  + ',
    deliverySumTitle: 'Ваша доставка',
    units: 'км',
    currency: '₽',
    tooltip: 'Выберите адрес доставки на карте или через поиск'
};
import type {DrawingStyle, LineStringGeometry, LngLat, PolygonGeometry, RouteFeature} from '@yandex/ymaps3-types';
import {Feature} from 'geojson';
import {OUT_OF_ZONES_PRICE, ZONES} from './variables';

export type MapZone = {
    style: DrawingStyle;
    geometry: PolygonGeometry;
    price: number;
    priority: number;
    name: 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>'});
    // 
    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 function getLineStringLength(geometry: LineStringGeometry) {
    const feature: Feature = {
        type: 'Feature',
        geometry,
        properties: {}
    };
    return turf.length(feature);
}

export function getOutOfZoneLineSlice(route: LineStringGeometry, zone: PolygonGeometry) {
    const splitPoints = turf.lineIntersect(zone, route);
    const outOfZoneLineSlice = turf.lineSlice(
        splitPoints.features[0].geometry.coordinates,
        route.coordinates[route.coordinates.length - 1],
        route
    );
    return outOfZoneLineSlice;
}

export function calculatePrice(route: RouteFeature) {
    let price: number;
    let outOfZoneLineLength: number;
    const finalPoint = route.geometry.coordinates[route.geometry.coordinates.length - 1];
    const sortedZones = ZONES.sort((a, b) => b.priority - a.priority);
    let currentZone: MapZone = null;

    for (const zone of sortedZones) {
        const pointIsInZones = turf.booleanPointInPolygon(finalPoint, zone.geometry);
        if (pointIsInZones) {
            currentZone = zone;
        }
    }

    if (currentZone) {
        price = currentZone.price;
    } else {
        const lastZone = sortedZones[0];
        const outOfZoneLineSlice = getOutOfZoneLineSlice(route.geometry, lastZone.geometry);
        outOfZoneLineLength = getLineStringLength(outOfZoneLineSlice.geometry as LineStringGeometry);
        price = lastZone.price + OUT_OF_ZONES_PRICE * outOfZoneLineLength;
        currentZone = lastZone;
    }

    return {
        price,
        outOfZoneLineLength,
        currentZone
    };
}
.info-window {
    padding: 8px 12px 8px 40px;
    border-radius: 12px;
    background-color: #313133;
    background-image: url('./info-icon.svg');
    background-position: 10px 8px;
    background-repeat: no-repeat;
    color: #f2f5fa;
    font-size: 14px;
    line-height: 20px;
    min-width: max-content;
}

.delivery-sum-window {
    width: 220px;
    padding: 10px 12px;
    background-color: #212326;
    border-radius: 12px;
    box-sizing: border-box;
}

.delivery-sum-title {
    color: #ffffff;
    font-size: 14px;
    font-weight: 400;
}

.delivery-sum-content {
    margin-top: 8px;
    font-weight: 500;
    font-size: 16px;
    color: #ffffff;
}

.delivery-sum-footer {
    font-weight: 500;
    font-size: 14px;
    color: #f2f5fa;
    opacity: 0.7;
}

.delivery-cost-window {
    margin-top: 16px;
    width: 220px;
    padding: 8px;
    background-color: #ffffff;
    border-radius: 12px;
    box-sizing: border-box;
    box-shadow: 0px 4px 12px 0px #5f69831a;
}

.delivery-cost-title {
    height: 40px;
    padding: 8px;
    font-weight: 500;
    font-size: 16px;
    box-sizing: border-box;
}

.delivery-cost-content {
    padding: 8px;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.delivery-cost-footer {
    box-sizing: border-box;
    padding: 4px 8px 8px 8px;
    height: 32px;
    font-size: 14px;
}

.delivery-item {
    display: flex;
    flex-direction: row;
    gap: 12px;
}

.delivery-item-colorbox {
    border-style: solid;
    border-width: 3px;
    box-sizing: border-box;
    width: 20px;
    height: 20px;
    border-radius: 4px;
}

.divider {
    border-top: 1px solid rgba(92, 94, 102, 0.14);
    border-bottom: none;
    border-radius: 8px;
    margin: 8px;
}

.hidden {
    display: none;
}