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/turf.min.js"></script>

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

    <script
      data-plugins="transform-modules-umd"
      data-presets="typescript"
      type="text/babel"
      src="../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 '@mappable-world/mappable-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 mappable.ready;
          const {
              MMap,
              MMapDefaultSchemeLayer,
              MMapDefaultFeaturesLayer,
              MMapFeature,
              MMapListener,
              MMapControls,
              MMapControl
          } = mappable;
          const {MMapDefaultMarker, MMapSearchControl} = await mappable.import('@mappable-world/mappable-default-ui-theme');

          class InfoMessageClass extends mappable.MMapComplexEntity<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 = mappable.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 mappable.MMapComplexEntity<{}> {
              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 = mappable.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 mappable.MMapComplexEntity<{}> {
              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 = mappable.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 MMap(
              // Pass the link to the HTMLElement of the container
              document.getElementById('app'),
              // Pass the map initialization parameters
              {location: LOCATION, showScaleInCopyrights: true},
              // Add a map scheme layer
              [new MMapDefaultSchemeLayer({}), new MMapDefaultFeaturesLayer({})]
          );
          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 MMapFeature({
              geometry: {type: 'LineString', coordinates: []},
              style: ROUTE_STYLES
          });

          const deliverySumControl = new DeliverySumControl({});

          const marker = new MMapDefaultMarker({
              coordinates: ROUTE_START,
              iconName: 'building',
              size: 'normal',
              color: {day: END_MARKER_COLOR, night: END_MARKER_COLOR}
          });

          ZONES.forEach((zone) => map.addChild(new MMapFeature(zone)));

          map.addChild(
              new MMapDefaultMarker({
                  coordinates: ROUTE_START,
                  iconName: 'malls',
                  size: 'normal',
                  color: {day: START_MARKER_COLOR, night: START_MARKER_COLOR}
              })
          );

          map.addChild(
              new MMapControls({position: 'top right'}, [
                  new MMapSearchControl({
                      searchResult: searchHandler
                  })
              ])
          );

          const leftControl = new MMapControls({position: 'top left', orientation: 'vertical'}, [
              new MMapControl({transparent: true}).addChild(new InfoMessageClass({text: TRANSLATIONS.tooltip})),
              new MMapControl({transparent: true}).addChild(new DeliveryCostControl({}))
          ]);
          map.addChild(leftControl);

          map.addChild(new MMapListener({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/turf.min.js"></script>
    <!-- To make the map appear, you must add your apikey -->
    <script src="https://js.api.mappable.world/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></script>

    <script
      data-plugins="transform-modules-umd"
      data-presets="react, typescript"
      type="text/babel"
      src="../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 '@mappable-world/mappable-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 @mappable-world/mappable-reactify
        const [mappableReact] = await Promise.all([
          mappable.import('@mappable-world/mappable-reactify'),
          mappable.ready
        ]);

        const reactify = mappableReact.reactify.bindTo(React, ReactDOM);
        const {
          MMap,
          MMapDefaultSchemeLayer,
          MMapDefaultFeaturesLayer,
          MMapFeature,
          MMapListener,
          MMapControls,
          MMapControl
        } = reactify.module(mappable);
        const {MMapDefaultMarker, MMapSearchControl} = reactify.module(
          await mappable.import('@mappable-world/mappable-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
            <MMap location={LOCATION} showScaleInCopyrights={true} ref={(x) => (map = x)}>
              {/* Add a map scheme layer */}
              <MMapDefaultSchemeLayer />
              <MMapDefaultFeaturesLayer />

              {ZONES.map((zone) => (
                <MMapFeature key={zone.name} style={zone.style} geometry={zone.geometry} />
              ))}

              {routeGeometry && <MMapFeature style={ROUTE_STYLES} geometry={routeGeometry} />}

              <MMapDefaultMarker
                color={{day: START_MARKER_COLOR, night: START_MARKER_COLOR}}
                size="normal"
                iconName="malls"
                coordinates={ROUTE_START}
              />

              {finishCoordinates && (
                <MMapDefaultMarker
                  size="normal"
                  color={{day: END_MARKER_COLOR, night: END_MARKER_COLOR}}
                  iconName="building"
                  coordinates={finishCoordinates}
                />
              )}

              <MMapControls position="top right">
                <MMapSearchControl searchResult={searchHandler} />
              </MMapControls>

              <MMapControls position="top left" orientation="vertical">
                <MMapControl transparent>
                  <div className="info-window">{TRANSLATIONS.tooltip}</div>
                </MMapControl>

                <MMapControl 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>
                </MMapControl>

                {currentZone && price && (
                  <MMapControl 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>
                  </MMapControl>
                )}
              </MMapControls>

              <MMapListener onClick={onMapClick} />
            </MMap>
          );
        }

        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/turf.min.js"></script>

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

    <script
      data-plugins="transform-modules-umd"
      data-presets="typescript"
      type="text/babel"
      src="../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 '@mappable-world/mappable-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 @mappable-world/mappable-vuefy
        const [mappableVue] = await Promise.all([mappable.import('@mappable-world/mappable-vuefy'), mappable.ready]);
        const vuefy = mappableVue.vuefy.bindTo(Vue);

        const {
          MMap,
          MMapDefaultSchemeLayer,
          MMapDefaultFeaturesLayer,
          MMapFeature,
          MMapListener,
          MMapControls,
          MMapControl
        } = vuefy.module(mappable);
        const {MMapDefaultMarker, MMapSearchControl} = vuefy.module(
          await mappable.import('@mappable-world/mappable-default-ui-theme')
        );

        const app = Vue.createApp({
          components: {
            MMap,
            MMapDefaultSchemeLayer,
            MMapDefaultFeaturesLayer,
            MMapFeature,
            MMapDefaultMarker,
            MMapControls,
            MMapControl,
            MMapListener,
            MMapSearchControl
          },
          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 -->
      <MMap
        :location="LOCATION"
        :showScaleInCopyrights="true"
        :ref="refMap"
      >
        <!-- Add a map scheme layer -->
        <MMapDefaultSchemeLayer/>
        <MMapDefaultFeaturesLayer/>

        <template v-for="zone in ZONES" :key="zone.name">
          <MMapFeature  :geometry="zone.geometry" :style="zone.style" />
        </template>

        <MMapFeature v-if="routeGeometry" :style="ROUTE_STYLES" :geometry="routeGeometry"/>

        <MMapDefaultMarker
          size="normal"
          iconName="malls"
          :coordinates="ROUTE_START"
          :color="{day: START_MARKER_COLOR, night: START_MARKER_COLOR}"
        />

        <MMapDefaultMarker
          v-if="finishCoordinates"
          size="normal"
          iconName="building"
          :coordinates="finishCoordinates"
          :color="{day: END_MARKER_COLOR, night: END_MARKER_COLOR}"
        />

        <MMapControls position="top right">
          <MMapSearchControl :searchResult="searchHandler" />
        </MMapControls>

        <MMapControls position="top left" orientation="vertical">
          <MMapControl :transparent="true">
            <div class="info-window">
              {{ TRANSLATIONS.tooltip }}
            </div>
          </MMapControl>

          <MMapControl :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>
          </MMapControl>

          <MMapControl :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>
          </MMapControl>
        </MMapControls>

        <MMapListener @click="onMapClick" />
      </MMap>
    `
        });
        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 '@mappable-world/mappable-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
mappable.ready.then(() => {
  mappable.import.registerCdn(
    'https://cdn.jsdelivr.net/npm/{package}',
    '@mappable-world/mappable-default-ui-theme@0.0'
  );
});

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

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

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

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

  return route;
}

export 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, MMapLocationRequest} from '@mappable-world/mappable-types';
import {MapZone} from './common';

export const LOCATION: MMapLocationRequest = {
  center: [55.2742, 25.1975], // starting position [lng, lat]
  zoom: 11.2 // starting zoom
};

export const ROUTE_START: LngLat = [55.2742, 25.1975];
export const END_MARKER_COLOR = '#313133';
export const START_MARKER_COLOR = '#2E4CE5';
export const ROUTE_STYLES: DrawingStyle = {
  simplificationRate: 0,
  stroke: [
    {color: '#34D9AD', width: 6},
    {color: '#000000', width: 8, opacity: 0.4}
  ],
  fill: '#34D9AD'
};

export const ZONES: Array<MapZone> = [
  {
    style: {
      simplificationRate: 0,
      stroke: [{color: '#EF9A7A', width: 3}],
      fill: 'rgba(239, 154, 122, 0.29)'
    },
    geometry: {
      type: 'Polygon',
      coordinates: [
        [
          [55.2402, 25.1875],
          [55.2475, 25.1981],
          [55.2592, 25.1911],
          [55.2664, 25.1985],
          [55.2776, 25.2155],
          [55.2941, 25.2124],
          [55.3005, 25.1988],
          [55.2958, 25.1952],
          [55.2923, 25.1904],
          [55.287, 25.1836],
          [55.2833, 25.1806],
          [55.2786, 25.1791],
          [55.2677, 25.1691],
          [55.2402, 25.1875]
        ]
      ]
    },
    price: 40,
    priority: 1,
    name: 'zone A'
  },
  {
    style: {
      simplificationRate: 0,
      stroke: [{color: '#EE5441', width: 3}],
      fill: 'rgba(238, 84, 65, 0.1)'
    },
    geometry: {
      type: 'Polygon',
      coordinates: [
        [
          [55.19, 25.1524],
          [55.2254, 25.1311],
          [55.2202, 25.1239],
          [55.2387, 25.1102],
          [55.2526, 25.1234],
          [55.2687, 25.1304],
          [55.2909, 25.1317],
          [55.317, 25.1384],
          [55.3314, 25.1485],
          [55.3361, 25.1536],
          [55.3192, 25.1761],
          [55.3133, 25.1831],
          [55.3099, 25.1957],
          [55.3091, 25.202],
          [55.3176, 25.2079],
          [55.3059, 25.2134],
          [55.3005, 25.2186],
          [55.294, 25.2245],
          [55.2874, 25.2313],
          [55.2763, 25.2367],
          [55.2679, 25.2415],
          [55.2619, 25.2482],
          [55.2557, 25.2517],
          [55.2526, 25.2452],
          [55.2467, 25.2392],
          [55.2555, 25.2323],
          [55.2539, 25.2287],
          [55.2488, 25.2321],
          [55.2453, 25.2261],
          [55.251, 25.221],
          [55.2431, 25.2133],
          [55.2338, 25.2247],
          [55.2272, 25.2236],
          [55.2228, 25.2176],
          [55.2259, 25.2146],
          [55.2307, 25.2161],
          [55.2339, 25.2125],
          [55.2304, 25.209],
          [55.2268, 25.2019],
          [55.19, 25.1524]
        ]
      ]
    },
    price: 80,
    priority: 2,
    name: 'zone B'
  },
  {
    style: {
      simplificationRate: 0,
      stroke: [{color: '#7B72A5', width: 3}],
      fill: 'rgba(123, 114, 165, 0.1)'
    },
    geometry: {
      type: 'Polygon',
      coordinates: [
        [
          [55.2648, 25.2848],
          [55.2681, 25.2817],
          [55.2683, 25.2756],
          [55.2716, 25.2698],
          [55.272, 25.2625],
          [55.279, 25.2746],
          [55.287, 25.2801],
          [55.2919, 25.2827],
          [55.2939, 25.2764],
          [55.3082, 25.2824],
          [55.3125, 25.2816],
          [55.3268, 25.2924],
          [55.3387, 25.3007],
          [55.3541, 25.2913],
          [55.3603, 25.2901],
          [55.4097, 25.2636],
          [55.4061, 25.2507],
          [55.4046, 25.2375],
          [55.4047, 25.2207],
          [55.4033, 25.2127],
          [55.3966, 25.1977],
          [55.3965, 25.1983],
          [55.3975, 25.1803],
          [55.3789, 25.1337],
          [55.3643, 25.1202],
          [55.3479, 25.1124],
          [55.3342, 25.1012],
          [55.3212, 25.0914],
          [55.3036, 25.075],
          [55.2934, 25.0642],
          [55.2832, 25.0602],
          [55.2413, 25.0555],
          [55.2247, 25.0534],
          [55.2103, 25.0471],
          [55.1977, 25.0487],
          [55.1978, 25.0643],
          [55.2149, 25.0833],
          [55.2326, 25.1025],
          [55.1882, 25.135],
          [55.1638, 25.1548],
          [55.2597, 25.2889],
          [55.2648, 25.2848]
        ]
      ]
    },
    price: 150,
    priority: 3,
    name: 'zone C'
  }
];

export const OUT_OF_ZONES_PRICE = 20;

export const TRANSLATIONS = {
  deliveryWindowTitle: 'Delivery cost',
  deliveryWindowFooter: 'Every extra 1 km  + ',
  deliverySumTitle: 'Your delivery',
  units: 'km',
  currency: 'AED',
  tooltip: 'Pick address on map or by search'
};