Measuring with a ruler using the ruler module

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>
    <!-- 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 [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={reactify.useDefault(RULER_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
];