Delivery cost calculator

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>

    <!-- prettier-ignore -->
    <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>

    <!-- prettier-ignore -->
    <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>

    <!-- prettier-ignore -->
    <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>
.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;
}
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
  };
}
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: 'Выберите адрес доставки на карте или через поиск'
};