Route progress

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"></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="./common.ts"
    ></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">
      import type {LngLat} from '@mappable-world/mappable-types';
      import {
          ANIMATE_DURATION_MS,
          DriverAnimation,
          angleFromCoordinate,
          animate,
          fetchRoute,
          splitLineString
      } from './common';
      import {
          INITIAL_DRIVER_SPEED,
          LOCATION,
          MARKER_IMAGE_PATH,
          MAX_DRIVER_SPEED,
          MIN_DRIVER_SPEED,
          PASSED_ROUTE_STYLE,
          ROUTE,
          ROUTE_STYLE
      } from '../variables';

      window.map = null;

      main();

      async function main() {
          // Waiting for all api elements to be loaded
          await mappable.ready;
          const {MMap, MMapDefaultSchemeLayer, MMapDefaultFeaturesLayer, MMapFeature, MMapMarker, MMapControls, MMapControl} =
              mappable;

          // Import the package to add a default marker
          const {MMapDefaultMarker} = await mappable.import('@mappable-world/mappable-default-ui-theme');

          // 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({}),
                  // Add a layer of geo objects to display the markers
                  new MMapDefaultFeaturesLayer({})
              ]
          );

          class ResetButton extends mappable.MMapComplexEntity<{onClick: () => void}> {
              private _element!: HTMLButtonElement;

              // Method for create a DOM control element
              _createElement() {
                  // Create a root element
                  const button = document.createElement('button');
                  button.classList.add('button');
                  button.innerText = 'Restart';
                  return button;
              }

              // Method for attaching the control to the map
              _onAttach() {
                  this._element = this._createElement();
                  this._element.addEventListener('click', this._onClick);

                  const control = new MMapControl({}, this._element);
                  this.addChild(control);
              }

              // Method for detaching control from the map
              _onDetach() {
                  this._element.removeEventListener('click', this._onClick);
              }

              _onClick = () => {
                  this._props.onClick();
              };
          }
          type SpeedRangeProps = {
              onChange: (value: number) => void;
              initialValue: number;
              min: number;
              max: number;
          };
          class SpeedRange extends mappable.MMapComplexEntity<SpeedRangeProps> {
              private _element!: HTMLDivElement;
              private _input!: HTMLInputElement;

              // Method for create a DOM control element
              _createElement() {
                  // Create a root element
                  const container = document.createElement('div');
                  container.classList.add('container');

                  const text = document.createElement('div');
                  text.classList.add('text');
                  text.innerText = 'speed';

                  this._input = document.createElement('input');
                  this._input.id = 'range';
                  this._input.type = 'range';
                  this._input.min = this._props.min.toString();
                  this._input.max = this._props.max.toString();
                  this._input.step = '1';
                  this._input.value = this._props.initialValue.toString();
                  this._input.classList.add('slider');
                  const percent = this.__getPercent(this._props.initialValue);
                  this._input.style.background = `linear-gradient(to right, #122DB2 ${percent}%, #F5F6F7 ${percent}%)`;
                  this._input.addEventListener('input', this._onInput);

                  container.appendChild(text);
                  container.appendChild(this._input);

                  return container;
              }

              __getPercent(value: number) {
                  return ((value - this._props.min) / (this._props.max - this._props.min)) * 100;
              }

              // Method for attaching the control to the map
              _onAttach() {
                  this._element = this._createElement();
                  const control = new MMapControl({transparent: true}, this._element);
                  this.addChild(control);
              }

              // Method for detaching control from the map
              _onDetach() {
                  this._input.removeEventListener('input', this._onInput);
              }

              _onInput = () => {
                  const value = Number(this._input.value);
                  this._props.onChange(value);
                  const percent = this.__getPercent(value);
                  this._input.style.background = `linear-gradient(to right, #122DB2 ${percent}%, #F5F6F7 ${percent}%)`;
              };
          }

          let animation: DriverAnimation;
          let driverSpeed = INITIAL_DRIVER_SPEED;
          let prevCoordinates: LngLat;

          const routeProgress = (initDistance: number) => {
              let passedDistance = initDistance;
              let passedTime = 0;
              animation = animate((progress) => {
                  const timeS = (progress * ANIMATE_DURATION_MS) / 1000;
                  const length = passedDistance + driverSpeed * (timeS - passedTime);

                  const nextCoordinates = turf.along(route.geometry, length, {units: 'meters'}).geometry
                      .coordinates as LngLat;

                  marker.update({coordinates: nextCoordinates});
                  if (prevCoordinates && !turf.booleanEqual(turf.point(prevCoordinates), turf.point(nextCoordinates))) {
                      const angle = angleFromCoordinate(prevCoordinates, nextCoordinates);
                      const markerElement = document.getElementById('marker');
                      markerElement.style.transform = `rotate(${angle}deg)`;
                  }

                  const [newLineStingFirstPart, newLineStringSecondPart] = splitLineString(route, nextCoordinates);
                  lineStringFirstPart.update({geometry: newLineStingFirstPart});
                  lineStringSecondPart.update({geometry: newLineStringSecondPart});

                  prevCoordinates = nextCoordinates;
                  passedTime = timeS;
                  passedDistance = length;

                  if (progress === 1 && routeLength > length) {
                      routeProgress(length);
                  }
              });
          };

          const lineStringSecondPart = new MMapFeature({
              geometry: {coordinates: [], type: 'LineString'},
              style: PASSED_ROUTE_STYLE
          });

          const lineStringFirstPart = new MMapFeature({
              geometry: {coordinates: [], type: 'LineString'},
              style: ROUTE_STYLE
          });

          map.addChild(new MMapDefaultMarker(ROUTE.start));
          map.addChild(new MMapDefaultMarker(ROUTE.end));

          const markerElement = document.createElement('div');
          markerElement.classList.add('marker_container');

          const markerElementImg = document.createElement('img');
          markerElementImg.src = MARKER_IMAGE_PATH;
          markerElementImg.alt = 'marker';
          markerElementImg.id = 'marker';
          markerElement.appendChild(markerElementImg);

          const marker = new MMapMarker(
              {
                  coordinates: ROUTE.start.coordinates,
                  disableRoundCoordinates: true
              },
              markerElement
          );
          map.addChild(marker);

          map.addChild(
              new MMapControls({position: 'bottom'}, [
                  new ResetButton({
                      onClick: () => {
                          const animationId = animation.getAnimationId();
                          cancelAnimationFrame(animationId);
                          marker.update({coordinates: ROUTE.start.coordinates});
                          routeProgress(0);
                      }
                  })
              ])
          ).addChild(
              new MMapControls({position: 'top right'}, [
                  new SpeedRange({
                      initialValue: INITIAL_DRIVER_SPEED,
                      min: MIN_DRIVER_SPEED,
                      max: MAX_DRIVER_SPEED,
                      onChange: (value) => {
                          driverSpeed = value;
                      }
                  })
              ])
          );

          const route = await fetchRoute(ROUTE.start.coordinates, ROUTE.end.coordinates);
          const routeLength = turf.length(turf.lineString(route.geometry.coordinates), {units: 'meters'});
          lineStringFirstPart.update({geometry: route.geometry});
          map.addChild(lineStringFirstPart);
          map.addChild(lineStringSecondPart);
          routeProgress(0);
      }
    </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@18/umd/react.production.min.js"></script>
    <script crossorigin src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js"></script>
    <script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
    <script crossorigin src="https://cdn.jsdelivr.net/npm/@turf/turf@7"></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="./common.ts"
    ></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">
      import type {LineStringGeometry, LngLat, RouteFeature} from '@mappable-world/mappable-types';
      import {
          ANIMATE_DURATION_MS,
          DriverAnimation,
          angleFromCoordinate,
          animate,
          fetchRoute,
          splitLineString
      } from './common';
      import {
          INITIAL_DRIVER_SPEED,
          LOCATION,
          MARKER_IMAGE_PATH,
          MAX_DRIVER_SPEED,
          MIN_DRIVER_SPEED,
          PASSED_ROUTE_STYLE,
          ROUTE,
          ROUTE_STYLE
      } from '../variables';
      import type {ChangeEvent, CSSProperties} from 'react';

      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, MMapMarker, MMapFeature, MMapControls, MMapControl} =
              reactify.module(mappable);

          // Import the package to add a default marker
          const {MMapDefaultMarker} = await reactify.module(await mappable.import('@mappable-world/mappable-default-ui-theme'));

          const {useState, useMemo, useEffect, useRef, useCallback} = React;

          const getPercent = (value: number) => {
              return ((value - MIN_DRIVER_SPEED) / (MAX_DRIVER_SPEED - MIN_DRIVER_SPEED)) * 100;
          };

          function App() {
              const animation = useRef<DriverAnimation>();
              const route = useRef<RouteFeature>();
              const routeLength = useRef<number>(0);
              const prevCoordinates = useRef<LngLat>();
              const driverSpeedRef = useRef(INITIAL_DRIVER_SPEED);

              const [coordinates, setCoordinates] = useState(ROUTE.start.coordinates);
              const [angle, setAngle] = useState(0);
              const [lineStringSecondPart, setLineStringSecondPart] = useState<LineStringGeometry>({
                  type: 'LineString',
                  coordinates: []
              });
              const [lineStringFirstPart, setLineStringFirstPart] = useState<LineStringGeometry>({
                  type: 'LineString',
                  coordinates: []
              });

              const [sliderStyle, setSliderStyle] = useState<CSSProperties>({
                  background: `linear-gradient(to right, #122DB2 ${getPercent(driverSpeedRef.current)}%, #F5F6F7 ${getPercent(
                      driverSpeedRef.current
                  )}%)`
              });

              const onRestartClick = useCallback(() => {
                  const animationId = animation.current.getAnimationId();
                  cancelAnimationFrame(animationId);
                  setCoordinates(ROUTE.start.coordinates);
                  routeProgress(0);
              }, []);

              const onSliderChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
                  const value = Number(event.target.value);
                  driverSpeedRef.current = value;
                  setSliderStyle({
                      background: `linear-gradient(to right, #122DB2 ${getPercent(value)}%, #F5F6F7 ${getPercent(value)}%)`
                  });
              }, []);

              useEffect(() => {
                  fetchRoute(ROUTE.start.coordinates, ROUTE.end.coordinates).then((routeRes) => {
                      route.current = routeRes;
                      routeLength.current = turf.length(turf.lineString(route.current.geometry.coordinates), {
                          units: 'meters'
                      });
                      setLineStringFirstPart(route.current.geometry);
                      routeProgress(0);
                  });
              }, []);

              const routeProgress = (initDistance: number) => {
                  let passedDistance = initDistance;
                  let passedTime = 0;
                  animation.current = animate((progress) => {
                      const timeS = (progress * ANIMATE_DURATION_MS) / 1000;
                      const length = passedDistance + driverSpeedRef.current * (timeS - passedTime);

                      const nextCoordinates = turf.along(route.current.geometry, length, {units: 'meters'}).geometry
                          .coordinates as LngLat;

                      setCoordinates(nextCoordinates);
                      if (
                          prevCoordinates.current &&
                          !turf.booleanEqual(turf.point(prevCoordinates.current), turf.point(nextCoordinates))
                      ) {
                          setAngle(angleFromCoordinate(prevCoordinates.current, nextCoordinates));
                      }

                      const [newLineStingFirstPart, newLineStringSecondPart] = splitLineString(
                          route.current,
                          nextCoordinates
                      );
                      setLineStringFirstPart(newLineStingFirstPart);
                      setLineStringSecondPart(newLineStringSecondPart);

                      prevCoordinates.current = nextCoordinates;
                      passedTime = timeS;
                      passedDistance = length;

                      if (progress === 1 && routeLength.current > length) {
                          routeProgress(length);
                      }
                  });
              };

              return (
                  // Initialize the map and pass initialization parameters
                  <MMap location={LOCATION} showScaleInCopyrights={true} ref={(x) => (map = x)}>
                      {/* Add a map scheme layer */}
                      <MMapDefaultSchemeLayer />
                      {/* Add a layer of geo objects to display the markers */}
                      <MMapDefaultFeaturesLayer />

                      <MMapDefaultMarker {...ROUTE.start} />
                      <MMapDefaultMarker {...ROUTE.end} />

                      <MMapFeature geometry={lineStringFirstPart} style={ROUTE_STYLE} />
                      <MMapFeature geometry={lineStringSecondPart} style={PASSED_ROUTE_STYLE} />

                      <MMapMarker disableRoundCoordinates coordinates={coordinates}>
                          <div className="marker_container">
                              <img src={MARKER_IMAGE_PATH} alt="marker" style={{transform: `rotate(${angle}deg)`}} />
                          </div>
                      </MMapMarker>

                      <MMapControls position="bottom">
                          <MMapControl>
                              <button onClick={onRestartClick} className="button">
                                  Restart
                              </button>
                          </MMapControl>
                      </MMapControls>
                      <MMapControls position="top right">
                          <MMapControl transparent={true}>
                              <div className="container">
                                  <div className="text">speed</div>
                                  <input
                                      style={sliderStyle}
                                      type="range"
                                      defaultValue={INITIAL_DRIVER_SPEED}
                                      min={MIN_DRIVER_SPEED}
                                      onChange={onSliderChange}
                                      max={MAX_DRIVER_SPEED}
                                      step="1"
                                      className="slider"
                                  />
                              </div>
                          </MMapControl>
                      </MMapControls>
                  </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"></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="./common.ts"
    ></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">
      import type {LineStringGeometry, LngLat, RouteFeature} from '@mappable-world/mappable-types';
      import {
          ANIMATE_DURATION_MS,
          DriverAnimation,
          angleFromCoordinate,
          animate,
          fetchRoute,
          splitLineString
      } from './common';
      import {
          INITIAL_DRIVER_SPEED,
          LOCATION,
          MARKER_IMAGE_PATH,
          MAX_DRIVER_SPEED,
          MIN_DRIVER_SPEED,
          PASSED_ROUTE_STYLE,
          ROUTE,
          ROUTE_STYLE
      } from '../variables';

      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, MMapMarker, MMapControls, MMapControl} =
              vuefy.module(mappable);

          // Import the package to add a default marker
          const {MMapDefaultMarker} = await vuefy.module(await mappable.import('@mappable-world/mappable-default-ui-theme'));

          const {ref, onMounted} = Vue;

          const getPercent = (value: number) => {
              return ((value - MIN_DRIVER_SPEED) / (MAX_DRIVER_SPEED - MIN_DRIVER_SPEED)) * 100;
          };

          const app = Vue.createApp({
              components: {
                  MMap,
                  MMapDefaultSchemeLayer,
                  MMapDefaultFeaturesLayer,
                  MMapFeature,
                  MMapDefaultMarker,
                  MMapMarker,
                  MMapControls,
                  MMapControl
              },
              setup() {
                  const refMap = (ref) => {
                      window.map = ref?.entity;
                  };

                  let animation: DriverAnimation;
                  let route: RouteFeature;
                  let routeLength = 0;
                  let prevCoordinates: LngLat;

                  const driverSpeed = ref(INITIAL_DRIVER_SPEED);
                  const coordinates = ref(ROUTE.start.coordinates);
                  const angle = ref(0);
                  const lineStringSecondPart = ref<LineStringGeometry>({
                      type: 'LineString',
                      coordinates: []
                  });
                  const lineStringFirstPart = ref<LineStringGeometry>({
                      type: 'LineString',
                      coordinates: []
                  });
                  const sliderStyle = ref({
                      background: `linear-gradient(to right, #122DB2 ${getPercent(driverSpeed.value)}%, #F5F6F7 ${getPercent(
                          driverSpeed.value
                      )}%)`
                  });

                  const onRestartClick = () => {
                      const animationId = animation.getAnimationId();
                      cancelAnimationFrame(animationId);
                      coordinates.value = ROUTE.start.coordinates;
                      routeProgress(0);
                  };

                  const onSliderChange = (event: Event) => {
                      const value = Number((event.target as HTMLInputElement).value);
                      sliderStyle.value = {
                          background: `linear-gradient(to right, #122DB2 ${getPercent(value)}%, #F5F6F7 ${getPercent(
                              value
                          )}%)`
                      };
                  };

                  onMounted(() => {
                      fetchRoute(ROUTE.start.coordinates, ROUTE.end.coordinates).then((routeRes) => {
                          route = routeRes;
                          routeLength = turf.length(turf.lineString(route.geometry.coordinates), {
                              units: 'meters'
                          });
                          lineStringFirstPart.value = route.geometry;
                          routeProgress(0);
                      });
                  });

                  const routeProgress = (initDistance: number) => {
                      let passedDistance = initDistance;
                      let passedTime = 0;
                      animation = animate((progress) => {
                          const timeS = (progress * ANIMATE_DURATION_MS) / 1000;
                          const length = passedDistance + driverSpeed.value * (timeS - passedTime);

                          const nextCoordinates = turf.along(route.geometry, length, {units: 'meters'}).geometry
                              .coordinates as LngLat;

                          coordinates.value = nextCoordinates;
                          if (
                              prevCoordinates &&
                              !turf.booleanEqual(turf.point(prevCoordinates), turf.point(nextCoordinates))
                          ) {
                              angle.value = angleFromCoordinate(prevCoordinates, nextCoordinates);
                          }

                          const [newLineStingFirstPart, newLineStringSecondPart] = splitLineString(route, nextCoordinates);
                          lineStringFirstPart.value = newLineStingFirstPart;
                          lineStringSecondPart.value = newLineStringSecondPart;

                          prevCoordinates = nextCoordinates;
                          passedTime = timeS;
                          passedDistance = length;

                          if (progress === 1 && routeLength > length) {
                              routeProgress(length);
                          }
                      });
                  };

                  return {
                      LOCATION,
                      ROUTE,
                      ROUTE_STYLE,
                      PASSED_ROUTE_STYLE,
                      MARKER_IMAGE_PATH,
                      INITIAL_DRIVER_SPEED,
                      MAX_DRIVER_SPEED,
                      MIN_DRIVER_SPEED,
                      refMap,
                      driverSpeed,
                      coordinates,
                      lineStringSecondPart,
                      lineStringFirstPart,
                      sliderStyle,
                      angle,
                      onRestartClick,
                      onSliderChange
                  };
              },
              template: `
                <!--Initialize the map and pass initialization parameters-->
                <MMap :location="LOCATION" :showScaleInCopyrights="true" :ref="refMap">
                  <!--Add a map scheme layer-->
                  <MMapDefaultSchemeLayer/>
                  <!-- Add a layer of geo objects to display the markers -->
                  <MMapDefaultFeaturesLayer/>

                  <MMapDefaultMarker v-bind="ROUTE.start" />
                  <MMapDefaultMarker v-bind="ROUTE.end" />

                  <MMapFeature :geometry="lineStringFirstPart" :style="ROUTE_STYLE"/>
                  <MMapFeature :geometry="lineStringSecondPart" :style="PASSED_ROUTE_STYLE" />

                  <MMapMarker disableRoundCoordinates :coordinates="coordinates">
                    <div class="marker_container">
                      <img
                        :src="MARKER_IMAGE_PATH"
                        alt="marker"
                        :style="{ transform: 'rotate(' + angle + 'deg)' }"
                      />
                    </div>
                  </MMapMarker>

                  <MMapControls position="bottom">
                    <MMapControl>
                      <button @click="onRestartClick" class="button">Restart</button>
                    </MMapControl>
                  </MMapControls>

                  <MMapControls position="top right">
                    <MMapControl :transparent="true">
                      <div class="container">
                        <div class="text">
                          speed
                        </div>
                        <input
                          type="range"
                          v-model="driverSpeed"
                          :min="MIN_DRIVER_SPEED"
                          @input="onSliderChange"
                          :max="MAX_DRIVER_SPEED"
                          step="1"
                          class="slider"
                          :style="sliderStyle"
                        />
                      </div>
                    </MMapControl>
                  </MMapControls>
                </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>
.marker_container {
  position: absolute;

  transform: translate(-50%, -50%) scale(0.5);
}

.container {
  display: flex;
  align-items: center;

  width: 210px;
  padding: 16px;

  border-radius: 12px;
  background: #fff;
  box-shadow: 0 4px 12px 0 rgba(95, 105, 131, 0.1), 0 4px 24px 0 rgba(95, 105, 131, 0.04);
  gap: 12px;
}

.text {
  font-size: 14px;
  font-style: normal;
  line-height: 16px;

  color: #050d33;
}

.button {
  width: 120px;
  height: 40px;
  margin: 0 auto;

  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  text-align: center;

  border: none;
  border-radius: 12px;
  background-color: #fff;
}

input[type='range'] {
  width: 100%;
  height: 2px;

  cursor: pointer;

  outline: none;
  background: linear-gradient(to right, #122db2 50%, #f5f6f7 50%);
  -webkit-appearance: none;
  appearance: none;
}

input[type='range']::-webkit-slider-thumb {
  width: 16px;
  height: 16px;

  cursor: pointer;

  border: 2px solid #122db2;
  border-radius: 50%;
  background-color: #fff;
  -webkit-appearance: none;
  appearance: none;
}
import type {LineStringGeometry, LngLat, RouteFeature} from '@mappable-world/mappable-types';

// 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 const ANIMATE_DURATION_MS = 4000;
export type DriverAnimation = {
  getAnimationId: () => number;
};

export function animate(cb: (progress: number) => void): DriverAnimation {
  let animationId = 0;
  const startTime = Date.now();
  function tick() {
    const progress = (Date.now() - startTime) / ANIMATE_DURATION_MS;
    if (progress >= 1) {
      cb(1);
      return;
    }

    cb(progress);
    animationId = requestAnimationFrame(tick);
  }

  animationId = requestAnimationFrame(tick);

  return {
    getAnimationId: () => animationId
  };
}

export function angleFromCoordinate(lngLat1: LngLat, lngLat2: LngLat) {
  const toRadians = (degrees: number) => degrees * (Math.PI / 180);
  const toDegrees = (radians: number) => radians * (180 / Math.PI);

  const dLon = toRadians(lngLat2[0] - lngLat1[0]);

  const y = Math.sin(dLon) * Math.cos(toRadians(lngLat2[1]));
  const x =
    Math.cos(toRadians(lngLat1[1])) * Math.sin(toRadians(lngLat2[1])) -
    Math.sin(toRadians(lngLat1[1])) * Math.cos(toRadians(lngLat2[1])) * Math.cos(dLon);

  let deg = Math.atan2(y, x);

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

  return deg;
}

export function splitLineString(route: RouteFeature, coordinates: LngLat) {
  if (!route || !coordinates) {
    return [];
  }
  const firstPart = turf.lineSlice(
    coordinates,
    route.geometry.coordinates[route.geometry.coordinates.length - 1],
    route.geometry
  );
  const secondPart = turf.lineSlice(route.geometry.coordinates[0], coordinates, route.geometry);
  return [firstPart.geometry as LineStringGeometry, secondPart.geometry as LineStringGeometry];
}
import type {DrawingStyle, MMapLocationRequest} from '@mappable-world/mappable-types';
import type {MMapDefaultMarkerProps} from '@mappable-world/mappable-default-ui-theme';

export const LOCATION: MMapLocationRequest = {
  center: [55.4355, 25.3461], // starting position [lng, lat]
  zoom: 14.0 // starting zoom
};

export const INITIAL_DRIVER_SPEED = 210;
export const MIN_DRIVER_SPEED = 20;
export const MAX_DRIVER_SPEED = 400;

export const ROUTE: {start: Partial<MMapDefaultMarkerProps>; end: Partial<MMapDefaultMarkerProps>} = {
  start: {
    zIndex: 1000,
    size: 'small',
    title: 'Home',
    iconName: 'building',
    coordinates: [55.4609, 25.3398]
  },
  end: {
    zIndex: 1000,
    size: 'normal',
    title: 'Al Hisn Fort',
    iconName: 'fallback',
    coordinates: [55.3862, 25.3587]
  }
};

export const ROUTE_STYLE: DrawingStyle = {
  simplificationRate: 0,
  stroke: [
    {color: '#34D9AD', width: 7},
    {color: '#050D33', opacity: 0.4, width: 9}
  ]
};

export const PASSED_ROUTE_STYLE: DrawingStyle = {
  simplificationRate: 0,
  stroke: [{color: '#050D33', opacity: 0.4, width: 9}]
};

export const MARKER_IMAGE_PATH = '../marker-black.png';