Measuring with a ruler using the ruler module
vanilla.html
react.html
vue.html
common.css
common.ts
variables.ts
<!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"></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
];