Measuring with a ruler using the ruler module

Open on 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>
    <!-- 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 type {MMapMarker} from '@mappable-world/mappable-types';
      import type {RenderPointArgs, RulerPointState} from '@mappable-world/mappable-types/modules/ruler';
      import {FEATURE_STYLE, formatArea, formatDistance, randomColor} from './common';
      import {LOCATION, RULER_COORDINATES} from '../variables';

      window.map = null;

      main();
      async function main() {
          // Waiting for all api elements to be loaded
          await mappable.ready;
          const {
              MMap,
              MMapDefaultSchemeLayer,
              MMapDefaultFeaturesLayer,
              MMapMarker,
              MMapControls,
              MMapControl,
              MMapControlButton
          } = mappable;
          const {MMapRuler} = await mappable.import('@mappable-world/mappable-ruler');
          // 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 infoElement = document.createElement('span');
          infoElement.classList.add('info');

          const rulerButtonElement = document.createElement('span');
          rulerButtonElement.classList.add('ruler-button');
          rulerButtonElement.textContent = 'ruler';
          const planimeterButtonElement = document.createElement('span');
          planimeterButtonElement.classList.add('planimeter-button');
          planimeterButtonElement.textContent = 'planimeter';

          const controlsRight = new MMapControls({position: 'top right'}, [
              new MMapControlButton({element: rulerButtonElement, onClick: () => ruler.update({type: 'ruler'})}),
              new MMapControlButton({element: planimeterButtonElement, onClick: () => ruler.update({type: 'planimeter'})})
          ]);
          const controlsLeft = new MMapControls({position: 'top left'}, [
              new MMapControlButton({text: 'enable', onClick: () => ruler.update({editable: true})}),
              new MMapControlButton({text: 'disable', onClick: () => ruler.update({editable: false})})
          ]);
          const controlsTop = new MMapControls({position: 'top'}, [new MMapControl({}, infoElement)]);
          map.addChild(controlsLeft).addChild(controlsRight).addChild(controlsTop);

          const previewPoint = document.createElement('div');
          previewPoint.classList.add('preview-point');

          // Ruler point entity
          class RulerPoint extends mappable.MMapComplexEntity<RenderPointArgs> {
              private _pointMarker: MMapMarker;
              private _balloonMarker: MMapMarker;
              private _balloonLabelElement: HTMLSpanElement;
              private _balloonElement: HTMLDivElement;
              private _state: RulerPointState;

              private _showBalloon = false;

              constructor(props: RenderPointArgs) {
                  super(props);

                  this._state = this._props.state;

                  const point = document.createElement('div');
                  point.classList.add('point');
                  point.style.backgroundColor = randomColor();

                  this._balloonElement = document.createElement('div');
                  this._balloonElement.classList.add('balloon');

                  this._balloonLabelElement = document.createElement('span');
                  this._balloonLabelElement.textContent = this.__getLabel();
                  this._balloonElement.appendChild(this._balloonLabelElement);
                  this.__toggleBalloon(this._state.index === this._state.totalCount - 1);

                  const deleteButton = document.createElement('button');
                  deleteButton.classList.add('button');
                  deleteButton.textContent = 'delete';
                  deleteButton.addEventListener('click', (event) => {
                      event.stopPropagation();
                      this._props.onDelete();
                  });
                  this._balloonElement.appendChild(deleteButton);

                  const {coordinates, editable, source} = this._state;

                  this._pointMarker = new MMapMarker(
                      {
                          coordinates,
                          draggable: editable,
                          source,
                          zIndex: 1,
                          onDragMove: this._props.onDragMove,
                          onFastClick: () => {
                              this._showBalloon = !this._showBalloon;
                              this.__toggleBalloon(this._showBalloon);
                          }
                      },
                      point
                  );
                  this._balloonMarker = new MMapMarker({coordinates, source, zIndex: 0}, this._balloonElement);
                  this.addChild(this._pointMarker).addChild(this._balloonMarker);
              }

              protected _onUpdate(props: Partial<RenderPointArgs>): void {
                  if (props.state !== undefined) {
                      this._state = props.state;
                      const {coordinates, editable, source} = this._state;

                      this._pointMarker.update({coordinates, draggable: editable, source});
                      this._balloonMarker.update({coordinates, source});

                      this._balloonLabelElement.textContent = this.__getLabel();
                      this.__toggleBalloon(
                          !this._showBalloon ? props.state.index === props.state.totalCount - 1 : this._showBalloon
                      );
                  }
              }

              private __getLabel(): string {
                  const {measurements} = this._state;
                  return measurements.type === 'ruler'
                      ? formatDistance(measurements.distance)
                      : formatArea(measurements.area);
              }

              private __toggleBalloon(show: boolean): void {
                  this._balloonElement.classList.toggle('hide', !show);
              }
          }

          const ruler = new MMapRuler({
              points: RULER_COORDINATES,
              type: 'ruler',
              editable: true,
              previewPoint,
              geometry: {style: FEATURE_STYLE},
              onUpdate: (state) => {
                  infoElement.textContent =
                      state.measurements.type === 'ruler'
                          ? `Total distance: ${formatDistance(state.measurements.totalDistance)}`
                          : `Area: ${formatArea(state.measurements.area)}`;
              },
              point: (props) => new RulerPoint(props)
          });
          map.addChild(ruler);
      }
    </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>
    <!-- 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="react, typescript"
      type="text/babel"
      src="./common.ts"
    ></script>
    <script data-plugins="transform-modules-umd" data-presets="react, typescript" type="text/babel">
      import type {LngLat} from '@mappable-world/mappable-types';
      import type {RenderPointArgs, RulerType, RulerCommonState, RulerGeometry} from '@mappable-world/mappable-types/modules/ruler';
      import {FEATURE_STYLE, formatArea, formatDistance, randomColor} from './common';
      import {LOCATION, RULER_COORDINATES} from '../variables';

      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,
              MMapControls,
              MMapControl,
              MMapControlButton
          } = reactify.module(mappable);
          const {MMapRuler} = reactify.module(await mappable.import('@mappable-world/mappable-ruler'));

          const RulerPoint = (props: RenderPointArgs) => {
              const {measurements, index, coordinates, editable, totalCount, source} = props.state;

              const [showBalloon, setShowBalloon] = React.useState(false);
              const backgroundColor = React.useRef(undefined);
              backgroundColor.current ??= randomColor();

              const label = React.useMemo(() => {
                  return measurements.type === 'planimeter'
                      ? formatArea(measurements.area)
                      : formatDistance(measurements.distance);
              }, [measurements]);

              const showLastBalloon = React.useMemo(() => {
                  return index === totalCount - 1;
              }, [index, totalCount]);

              const onClick = React.useCallback(() => {
                  setShowBalloon((value) => !value);
              }, []);

              const onDelete = React.useCallback((event) => {
                  event.stopPropagation();
                  props.onDelete();
              }, []);

              return (
                  <>
                      <MMapMarker
                          coordinates={coordinates}
                          zIndex={21}
                          source={source}
                          draggable={editable}
                          onDragMove={props.onDragMove}
                          onFastClick={onClick}
                      >
                          <div className="point" style={{backgroundColor: backgroundColor.current}}></div>
                      </MMapMarker>
                      {(showLastBalloon || showBalloon) && (
                          <MMapMarker coordinates={coordinates} zIndex={20} source={source}>
                              <div className="balloon">
                                  <span>{label}</span>
                                  <button onClick={onDelete} className="button">
                                      delete
                                  </button>
                              </div>
                          </MMapMarker>
                      )}
                  </>
              );
          };

          function App() {
              const [location, setLocation] = React.useState(LOCATION);
              const [geometry] = React.useState<RulerGeometry>({style: FEATURE_STYLE});
              const [type, setType] = React.useState<RulerType>('ruler');
              const [editable, setEditable] = React.useState(true);
              const [coordinates, setCoordinates] = React.useState<LngLat[]>(RULER_COORDINATES);
              const [commonInfo, setCommonInfo] = React.useState('');

              const setRulerType = React.useCallback(() => setType('ruler'), []);
              const setPlanimeterType = React.useCallback(() => setType('planimeter'), []);

              const enableRuler = React.useCallback(() => setEditable(true), []);
              const disableRuler = React.useCallback(() => setEditable(false), []);

              const onRender = React.useCallback((params: RenderPointArgs) => {
                  return <RulerPoint {...params} />;
              }, []);
              const onUpdate = React.useCallback(({measurements}: RulerCommonState) => {
                  setCommonInfo(
                      measurements.type === 'ruler'
                          ? `Total distance: ${formatDistance(measurements.totalDistance)}`
                          : `Area: ${formatArea(measurements.area)}`
                  );
              }, []);

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

                      <MMapControls position="top right">
                          <MMapControlButton onClick={setRulerType}>
                              <span className="ruler-button">ruler</span>
                          </MMapControlButton>
                          <MMapControlButton onClick={setPlanimeterType}>
                              <span className="planimeter-button">planimeter</span>
                          </MMapControlButton>
                      </MMapControls>

                      <MMapControls position="top left">
                          <MMapControlButton text="enable" onClick={enableRuler} />
                          <MMapControlButton text="disable" onClick={disableRuler} />
                      </MMapControls>

                      <MMapControls position="top">
                          <MMapControl>
                              <span className="info">{commonInfo}</span>
                          </MMapControl>
                      </MMapControls>

                      <MMapRuler
                          points={coordinates}
                          editable={editable}
                          type={type}
                          point={onRender}
                          onUpdate={onUpdate}
                          geometry={geometry}
                          previewPoint={<div className="preview-point"></div>}
                      />
                  </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 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 type {LngLat} from '@mappable-world/mappable-types';
      import type {RenderPointArgs, RulerCommonState, RulerType} from '@mappable-world/mappable-types/modules/ruler';
      import type TVue from 'vue';
      import {FEATURE_STYLE, formatArea, formatDistance, randomColor} from './common';
      import {LOCATION, RULER_COORDINATES} 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,
              MMapMarker,
              MMapListener,
              MMapControls,
              MMapControl,
              MMapControlButton
          } = vuefy.module(mappable);
          const {MMapRuler} = vuefy.module(await mappable.import('@mappable-world/mappable-ruler'));

          const RulerPoint = Vue.defineComponent({
              name: 'RulerPoint',
              props: {
                  state: Object as TVue.PropType<RenderPointArgs['state']>,
                  onDragMove: Function as TVue.PropType<RenderPointArgs['onDragMove']>,
                  onDelete: Function as TVue.PropType<RenderPointArgs['onDelete']>
              },
              components: {MMapMarker},
              setup(props) {
                  const showBalloon = Vue.ref(false);
                  const label = Vue.computed(() => {
                      return props.state.measurements.type === 'planimeter'
                          ? formatArea(props.state.measurements.area)
                          : formatDistance(props.state.measurements.distance);
                  });

                  const showLastBalloon = Vue.computed(() => {
                      return props.state.index === props.state.totalCount - 1;
                  });

                  const onClick = () => {
                      showBalloon.value = !showBalloon.value;
                  };

                  const onDeleteHandler = (event) => {
                      event.stopPropagation();
                      props.onDelete();
                  };

                  const backgroundColor = randomColor();

                  return {label, showBalloon, showLastBalloon, backgroundColor, onClick, onDeleteHandler};
              },
              template: `
                  <template>
                      <MMapMarker
                          :key="state.index"
                          :coordinates="state.coordinates"
                          :zIndex="21"
                          :draggable="state.editable"
                          :source="state.source"
                          :onDragMove="onDragMove"
                          :onFastClick="onClick">
                          <div class="point" :style="{backgroundColor}"></div>
                      </MMapMarker>
                      <MMapMarker v-if="showBalloon || showLastBalloon" :coordinates="state.coordinates" :source="state.source" :zIndex="20">
                          <div class="balloon">
                              <span>{{ label }}</span>
                              <button @click="onDeleteHandler" class="button">delete</button>
                          </div>
                      </MMapMarker>
                  </template>`
          });

          const app = Vue.createApp({
              components: {
                  MMap,
                  MMapDefaultSchemeLayer,
                  MMapDefaultFeaturesLayer,
                  MMapMarker,
                  MMapListener,
                  MMapControls,
                  MMapControl,
                  MMapControlButton,
                  MMapRuler,
                  RulerPoint
              },
              setup() {
                  const refMap = (ref) => {
                      window.map = ref?.entity;
                  };
                  const type = Vue.ref<RulerType>('ruler');
                  const editable = Vue.ref(true);
                  const coordinates = Vue.ref<LngLat[]>(RULER_COORDINATES);
                  const commonInfo = Vue.ref('');

                  const setRulerType = () => {
                      type.value = 'ruler';
                  };
                  const setPlanimeterType = () => {
                      type.value = 'planimeter';
                  };
                  const onUpdate = ({measurements}: RulerCommonState) => {
                      commonInfo.value =
                          measurements.type === 'ruler'
                              ? `Total distance: ${formatDistance(measurements.totalDistance)}`
                              : `Area: ${formatArea(measurements.area)}`;
                  };

                  return {
                      LOCATION,
                      FEATURE_STYLE,
                      refMap,
                      type,
                      coordinates,
                      editable,
                      commonInfo,
                      onUpdate,
                      setRulerType,
                      setPlanimeterType,
                      formatDistance,
                      formatArea
                  };
              },
              template: `
                  <MMap :location="LOCATION" :showScaleInCopyrights="true" :ref="refMap">
                      <MMapDefaultSchemeLayer />
                      <MMapDefaultFeaturesLayer />

                      <MMapControls position="top right">
                          <MMapControlButton :onClick="setRulerType">
                              <span class="ruler-button">ruler</span>
                          </MMapControlButton>
                          <MMapControlButton :onClick="setPlanimeterType">
                              <span class="planimeter-button">planimeter</span>
                          </MMapControlButton>
                      </MMapControls>

                      <MMapControls position="top left">
                          <MMapControlButton text="enable" :onClick="() => editable = true" />
                          <MMapControlButton text="disable" :onClick="() => editable = false" />
                      </MMapControls>

                      <MMapControls position="top">
                          <MMapControl>
                              <span className="info">{{commonInfo}}</span>
                          </MMapControl>
                      </MMapControls>

                      <MMapRuler
                          :points="coordinates"
                          :editable="editable"
                          :geometry="{style: FEATURE_STYLE}"
                          :type="type"
                          :onUpdate="onUpdate">
                          <template #point="params">
                              <RulerPoint v-bind="params" />
                          </template>
                          <template #previewPoint>
                              <div class="preview-point"></div>
                          </template>
                      </MMapRuler>
                  </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>
.point {
  position: absolute;

  display: inline-block;

  box-sizing: border-box;
  width: 16px;
  height: 16px;

  cursor: pointer;

  border: 2px solid #171b26;
  border-radius: 50%;
  background-color: #fefefe;

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

.balloon {
  position: absolute;
  z-index: 10;

  padding: 4px;

  font-size: 12px;
  white-space: nowrap;

  border-radius: 8px;
  background-color: #fefefe;
  box-shadow: 0 2px 4px 0 rgba(95, 105, 131, 0.2), 0 0 2px 0 rgba(95, 105, 131, 0.08);

  transform: translate(-50%, calc(-100% - 5px));
}

.hide {
  display: none;
}

.button {
  display: inline-block;

  margin: 0;
  padding: 4px 5px;

  list-style: none;

  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
  user-select: none;
  text-align: center;
  vertical-align: baseline;
  white-space: nowrap;

  color: #333;
  border-width: 0;
  border-radius: 8px;
  background-color: rgba(51, 51, 51, 0.05);

  transition: all 200ms;
  touch-action: manipulation;
}

.preview-point {
  position: absolute;

  box-sizing: border-box;
  width: 12px;
  height: 12px;

  cursor: pointer;

  opacity: 0.6;
  border: 1px solid #fff;
  border-radius: 50%;
  background: #666;

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

.info {
  display: inline-block;

  padding: 8px;

  font-size: 12px;
}
import type {DrawingStyle} from '@mappable-world/mappable-types';

export const FEATURE_STYLE: DrawingStyle = {
  simplificationRate: 0,
  fill: '#666',
  fillOpacity: 0.3,
  stroke: [
    {width: 3, opacity: 0.7, color: '#666'},
    {width: 5, opacity: 0.7, color: '#fff'}
  ]
};

export function formatDistance(distance: number): string {
  return distance > 900 ? `${roundDistance(distance / 1000)} km` : `${roundDistance(distance)} m`;
}

export function formatArea(area: number): string {
  return area > 900_000
    ? `${splitNumber(roundDistance(area / 1_000_000))} km²`
    : `${splitNumber(roundDistance(area))} m²`;
}

function roundDistance(distance: number): number {
  if (distance > 100) {
    return Math.round(distance);
  }
  const factor = Math.pow(10, distance > 10 ? 1 : 2);
  return Math.round(distance * factor) / factor;
}

function splitNumber(value: number): string {
  return value.toString().replace(/(\d)(?=(\d{3})+$)/g, '$1 ');
}

// Function for generating a pseudorandom number
const seed = (s: number) => () => {
  s = Math.sin(s) * 10000;
  return s - Math.floor(s);
};
const rnd = seed(10000); // () => Math.random()

export function randomColor() {
  return '#' + (((1 << 24) * rnd()) | 0).toString(16).padStart(6, '0');
}
import type {LngLat, MMapLocationRequest} from '@mappable-world/mappable-types';

export const LOCATION: MMapLocationRequest = {
  center: [31.245384, 30.051434], // starting position [lng, lat]
  zoom: 3 // starting zoom
};

export const RULER_COORDINATES: LngLat[] = [
  [-0.128407, 51.506807], // London
  [31.245384, 30.051434], // Cairo
  [77.201224, 28.614653] // New Delhi
];
{"node": 20, "template": "static"}