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

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

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

        <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, 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'
};
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
    };
}
.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;
}