Create a marker clusterer
vanilla.html
react.html
vue.html
common.css
common.ts
variables.css
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="react, typescript"
type="text/babel"
src="./common.ts"
></script>
<script
data-plugins="transform-modules-umd"
data-presets="react, typescript"
type="text/babel"
src="../variables.ts"
></script>
<script data-plugins="transform-modules-umd" data-presets="typescript" type="text/babel">
import type {LngLat} from '@mappable-world/mappable-types';
import type {Feature} from '@mappable-world/mappable-clusterer';
import {COMMON_LOCATION_PARAMS, getBounds, getRandomPoints, MARGIN} from './common';
import {BOUNDS, LOCATION} from '../variables';
window.map = null;
main();
async function main() {
// Waiting for all api elements to be loaded
await mappable.ready;
interface ClustererChangeControlProps {
toggleClusterer: () => void;
changePointsCount: (count: number) => void;
updatePoints: () => void;
}
class ClustererChangeControl extends mappable.MMapComplexEntity<ClustererChangeControlProps> {
private _element: HTMLDivElement;
private _detachDom: () => void;
// Method for create a DOM control element
_createElement(props: ClustererChangeControlProps) {
const {toggleClusterer, changePointsCount, updatePoints} = props;
const clustererChange = document.createElement('div');
clustererChange.classList.add('clusterer-change');
const inputSection = document.createElement('div');
inputSection.classList.add('clusterer-change__section');
const inputLabel = document.createElement('div');
inputLabel.classList.add('clusterer-change__input__label');
inputLabel.textContent = 'Point count:';
inputSection.appendChild(inputLabel);
const inputField = document.createElement('input');
inputField.type = 'number';
inputField.classList.add('clusterer-change__input');
inputField.value = '100';
inputField.addEventListener('input', (e: Event) => {
const target = e.target as HTMLInputElement;
changePointsCount(Number(target.value));
});
inputSection.appendChild(inputField);
const btnSection = document.createElement('div');
btnSection.classList.add('clusterer-change__buttons');
const updatePointsBtn = document.createElement('button');
updatePointsBtn.type = 'button';
updatePointsBtn.classList.add('clusterer-change__btn');
updatePointsBtn.textContent = 'Update points';
updatePointsBtn.addEventListener('click', updatePoints);
btnSection.appendChild(updatePointsBtn);
const toggleClustererBtn = document.createElement('button');
toggleClustererBtn.type = 'button';
toggleClustererBtn.id = 'toggleBtn';
toggleClustererBtn.classList.add('clusterer-change__btn');
toggleClustererBtn.textContent = 'Disable cluster mode';
toggleClustererBtn.addEventListener('click', toggleClusterer);
btnSection.appendChild(toggleClustererBtn);
const dividerElement = document.createElement('hr');
dividerElement.classList.add('divider');
clustererChange.appendChild(inputSection);
clustererChange.appendChild(dividerElement);
clustererChange.appendChild(btnSection);
return clustererChange;
}
// Method for attaching the control to the map
_onAttach() {
this._element = this._createElement(this._props);
this._detachDom = mappable.useDomContext(this, this._element, this._element);
}
// Method for detaching control from the map
_onDetach() {
this._detachDom();
this._detachDom = null;
this._element = null;
}
}
const {MMap, MMapDefaultSchemeLayer, MMapDefaultFeaturesLayer, MMapMarker, MMapControls, MMapControl} = mappable;
// Load the package with the cluster, extract the classes for creating clusterer objects and the clustering method
const {MMapClusterer, clusterByGrid} = await mappable.import('@mappable-world/mappable-clusterer');
const {MMapDefaultMarker} = await mappable.import('@mappable-world/mappable-default-ui-theme');
// Declare number of points in the clusterer
let pointsCount = 100;
let points = getRandomPoints(pointsCount, BOUNDS);
let markers = [];
map = new MMap(document.getElementById('app'), {location: LOCATION, showScaleInCopyrights: true, margin: MARGIN}, [
new MMapDefaultSchemeLayer({}),
new MMapDefaultFeaturesLayer({})
]);
/* We declare the function for rendering ordinary markers, we will submit it to the clusterer settings.
Note that the function must return any Entity element. In the example, this is MMapDefaultMarker. */
const marker = (feature: Feature) =>
new MMapDefaultMarker({
coordinates: feature.geometry.coordinates,
iconName: 'landmark'
});
// As for ordinary markers, we declare a cluster rendering function that also returns an Entity element.
const cluster = (coordinates: LngLat, features: Feature[]) =>
new MMapMarker(
{
coordinates,
onClick() {
const bounds = getBounds(features.map((feature: Feature) => feature.geometry.coordinates));
map.update({location: {bounds, ...COMMON_LOCATION_PARAMS}});
}
},
circle(features.length).cloneNode(true) as HTMLElement
);
function circle(count: number) {
const circle = document.createElement('div');
circle.classList.add('circle');
circle.innerHTML = `
<div class="circle-content">
<span class="circle-text">${count}</span>
</div>
`;
return circle;
}
/* We create a clusterer object and add it to the map object.
As parameters, we pass the clustering method, an array of features, the functions for rendering markers and clusters.
For the clustering method, we will pass the size of the grid division in pixels. */
const clusterer = new MMapClusterer({
method: clusterByGrid({gridSize: 64}),
features: points,
marker,
cluster
});
map.addChild(clusterer);
// Creating handler functions for changing the clusterer. We will use these functions in a custom control
// THe handler function for changing the number of clusterer points
function changePointsCount(count: number) {
pointsCount = count;
}
// The handler function for updating coordinates of clusterer points
function updatePoints() {
points = getRandomPoints(pointsCount, map.bounds);
clusterer.update({features: points});
if (!clusterer.parent) {
markers.forEach((markerEl) => {
map.removeChild(markerEl);
});
markers = [];
points.forEach((feature) => {
const markerElement = marker(feature);
map.addChild(markerElement);
markers.push(markerElement);
});
}
}
// The handler function for attach/detach the clusterer
function toggleClusterer() {
const button = document.getElementById('toggleBtn');
if (clusterer.parent) {
map.removeChild(clusterer);
points.forEach((feature) => {
const markerElement = marker(feature);
map.addChild(markerElement);
markers.push(markerElement);
});
button.innerText = 'Enable cluster mode';
} else {
map.addChild(clusterer);
markers.forEach((markerEl) => {
map.removeChild(markerEl);
});
markers = [];
button.innerText = 'Disable cluster mode';
}
}
// Creating and adding a custom clusterer change element to the map
map.addChild(
new MMapControls({position: 'top right'}).addChild(
new MMapControl().addChild(new ClustererChangeControl({toggleClusterer, changePointsCount, updatePoints}))
)
);
}
</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" />
<link rel="stylesheet" href="../variables.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="react, typescript"
type="text/babel"
src="./common.ts"
></script>
<script
data-plugins="transform-modules-umd"
data-presets="react, typescript"
type="text/babel"
src="../variables.ts"
></script>
<script data-plugins="transform-modules-umd" data-presets="react, typescript" type="text/babel">
import type {Feature} from '@mappable-world/mappable-clusterer';
import type {LngLat} from '@mappable-world/mappable-types';
import type TReact from 'react';
import {COMMON_LOCATION_PARAMS, getBounds, getRandomPoints, MARGIN} from './common';
import {BOUNDS, LOCATION} 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} =
reactify.module(mappable);
// Load the package with the cluster, extract the classes for creating clusterer objects and the clustering method
const {MMapClusterer, clusterByGrid} = reactify.module(
await mappable.import('@mappable-world/mappable-clusterer')
);
const {MMapDefaultMarker} = reactify.module(
await mappable.import('@mappable-world/mappable-default-ui-theme')
);
const {useState, useMemo, useCallback} = React;
function App() {
// We declare the initial states of the clusterer
// Clusterer visibility
const [isClusterer, setIsClusterer] = useState(true);
const [location, setLocation] = useState(LOCATION);
// Number of points in the clusterer
const [pointsCount, setPointsCount] = useState(100);
// Array with parameters for each clusterer point
const [points, setPoints] = useState(getRandomPoints(pointsCount, BOUNDS));
// We declare a render function. For the clustering method, we pass and store the size of one grid division in pixels
const gridSizedMethod = useMemo(() => clusterByGrid({gridSize: 64}), []);
const onClusterClick = useCallback(
(features: Feature[]) => {
const bounds = getBounds(features.map((feature: Feature) => feature.geometry.coordinates));
setLocation((prevLocation) => ({...prevLocation, bounds, ...COMMON_LOCATION_PARAMS}));
},
[location]
);
// We declare a function for rendering markers. Note that the function must return any Entity element. In the example, this is MMapDefaultMarker
const marker = (feature: Feature) => (
<MMapDefaultMarker iconName="landmark" coordinates={feature.geometry.coordinates} />
);
// We declare a cluster rendering function that also returns an Entity element. We will transfer the marker and cluster rendering functions to the clusterer settings
const cluster = (coordinates: LngLat, features: Feature[]) => (
<MMapMarker coordinates={coordinates}>
<div className="circle" onClick={() => onClusterClick(features)}>
<div className="circle-content">
<span className="circle-text">{features.length}</span>
</div>
</div>
</MMapMarker>
);
// Creating handler functions for changing the clusterer. We will use these functions in a custom control
// THe handler function for changing the number of clusterer points
const changePointsCount = useCallback(
(event: TReact.ChangeEvent<HTMLInputElement>) => setPointsCount(Number(event.target.value)),
[]
);
// The handler function for updating coordinates of clusterer points
const updatePoints = useCallback(() => setPoints(getRandomPoints(pointsCount, map.bounds)), [pointsCount]);
// The handler function for attach/detach the clusterer
const toggleClusterer = useCallback(() => setIsClusterer((prevValue) => !prevValue), []);
return (
// Initialize the map and pass initialization parameters
<MMap margin={MARGIN} location={location} showScaleInCopyrights={true} ref={(x) => (map = x)}>
{/* Add a map scheme layer */}
<MMapDefaultSchemeLayer />
{/* Add default feature layer */}
<MMapDefaultFeaturesLayer />
{/* In the clusterer props, we pass the previously declared functions for rendering markers and clusters,
the clustering method, and an array of features */}
{isClusterer ? (
<MMapClusterer marker={marker} cluster={cluster} method={gridSizedMethod} features={points} />
) : (
points.map((feature) => marker(feature))
)}
{/* Add a custom clusterer change element to the map */}
<MMapControls position="top right">
<MMapControl>
<div className="clusterer-change">
<div className="clusterer-change__section">
<div className="clusterer-change__input__label">Point count:</div>
<input
type="number"
className="clusterer-change__input"
value={pointsCount}
onChange={changePointsCount}
/>
</div>
<hr className="divider" />
<div className="clusterer-change__buttons">
<button type="button" className="clusterer-change__btn" onClick={updatePoints}>
Update points
</button>
<button type="button" className="clusterer-change__btn" onClick={toggleClusterer}>
{isClusterer ? 'Disable cluster mode' : 'Enable cluster mode'}
</button>
</div>
</div>
</MMapControl>
</MMapControls>
</MMap>
);
}
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('app')
);
}
</script>
<!-- prettier-ignore -->
<style> html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; } .toolbar { position: absolute; z-index: 1000; top: 0; left: 0; display: flex; align-items: center; padding: 16px; } .toolbar a { padding: 16px; } </style>
<link rel="stylesheet" href="./common.css" />
<link rel="stylesheet" href="../variables.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="react, typescript"
type="text/babel"
src="./common.ts"
></script>
<script
data-plugins="transform-modules-umd"
data-presets="react, typescript"
type="text/babel"
src="../variables.ts"
></script>
<script data-plugins="transform-modules-umd" data-presets="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" />
<link rel="stylesheet" href="../variables.css" />
</head>
<body>
<div id="app"></div>
</body>
</html>
.clusterer-change {
padding: 8px 16px;
border-radius: 12px;
display: flex;
flex-direction: column;
align-items: center;
}
.clusterer-change__section {
display: flex;
align-items: center;
width: 100%;
justify-content: space-between;
gap: 10px;
height: 48px;
}
.clusterer-change__buttons {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px 0;
}
.clusterer-change__input__label {
font-size: 14px;
}
.clusterer-change__input {
max-width: 60px;
height: 32px;
font-size: 16px;
text-align: center;
border: none;
border-radius: 8px;
background: rgba(92, 94, 102, 0.06);
font-size: 16px;
outline: none;
transition: border-color 0.2s ease;
}
.clusterer-change__btn {
border: none;
cursor: pointer;
width: 172px;
padding: 8px 16px;
color: #050d33;
font-size: 14px;
font-weight: 15;
background-color: rgba(92, 94, 102, 0.06);
border-radius: 8px;
transition: background-color 0.2s;
}
.clusterer-change__btn:hover {
background-color: rgba(92, 94, 102, 0.06);
}
.clusterer-change__btn:active {
background-color: rgba(92, 94, 102, 0.06);
}
.circle {
position: absolute;
width: 40px;
height: 40px;
color: var(--interact-action);
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.7);
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
transform: translate(-50%, -50%);
}
.circle:hover {
color: #2e4ce5;
cursor: pointer;
}
.circle-content {
position: absolute;
top: 50%;
left: 50%;
display: flex;
justify-content: center;
align-items: center;
width: 90%;
height: 90%;
border-radius: 50%;
background-color: currentColor;
transform: translate3d(-50%, -50%, 0);
}
.circle-text {
font-size: 16px;
font-weight: 500;
line-height: 20px;
color: #fff;
}
.pin {
transform: translate(-50%, -100%);
}
.divider {
width: 100%;
border: none;
border-top: 1px solid rgba(92, 94, 102, 0.1);
margin: 8px 0;
}
import type {LngLatBounds, LngLat, MMapLocationRequest, Margin} from '@mappable-world/mappable-types';
import type {Feature} from '@mappable-world/mappable-clusterer';
mappable.ready.then(() => {
mappable.import.registerCdn('https://cdn.jsdelivr.net/npm/{package}', [
'@mappable-world/mappable-default-ui-theme@0.0',
'@mappable-world/mappable-clusterer@0.0'
]);
});
// 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()
// Generating random coordinates of a point [lng, lat] in a given boundary
const getRandomPointCoordinates = (bounds: LngLatBounds): LngLat => [
bounds[0][0] + (bounds[1][0] - bounds[0][0]) * rnd(),
bounds[1][1] + (bounds[0][1] - bounds[1][1]) * rnd()
];
// A function that creates an array with parameters for each clusterer random point
export const getRandomPoints = (count: number, bounds: LngLatBounds): Feature[] => {
return Array.from({length: count}, (_, index) => ({
type: 'Feature',
id: index.toString(),
geometry: {type: 'Point', coordinates: getRandomPointCoordinates(bounds)}
}));
};
export const COMMON_LOCATION_PARAMS: Partial<MMapLocationRequest> = {easing: 'ease-in-out', duration: 2000};
export function getBounds(coordinates: number[][]): LngLatBounds {
let minLat = Infinity,
minLng = Infinity;
let maxLat = -Infinity,
maxLng = -Infinity;
for (const coords of coordinates) {
const lat = coords[1];
const lng = coords[0];
if (lat < minLat) minLat = lat;
if (lat > maxLat) maxLat = lat;
if (lng < minLng) minLng = lng;
if (lng > maxLng) maxLng = lng;
}
return [
[minLng, minLat],
[maxLng, maxLat]
] as LngLatBounds;
}
export const MARGIN: Margin = [100, 100, 100, 100];
:root {
--interact-action: #313133;
}
import type {LngLatBounds, MMapLocationRequest} from '@mappable-world/mappable-types';
/* Rectangle bounded by bottom-left and top-right coordinates
Inside it, we generate the first bundle of clusterer points */
export const BOUNDS: LngLatBounds = [
[55.2825, 25.2317],
[55.4132, 25.1753]
];
export const LOCATION: MMapLocationRequest = {
zoom: 11.6,
center: [55.3948, 25.1947]
};