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>

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

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

        <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 {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
];
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');
}
.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;
}