Create a marker clusterer
vanilla.html
react.html
vue.html
common.css
common.ts
variables.ts
sandbox.json
<!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 {LngLat} from '@mappable-world/mappable-types';
import type {Feature} from '@mappable-world/mappable-clusterer';
import {getRandomPoints} 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;
// Creating a custom control class for cluster management
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(+target.value);
});
inputSection.appendChild(inputField);
const btnSection = document.createElement('div');
btnSection.classList.add('clusterer-change__section');
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.classList.add('clusterer-change__btn');
toggleClustererBtn.textContent = 'Delete/Add Clusterer';
toggleClustererBtn.addEventListener('click', toggleClusterer);
btnSection.appendChild(toggleClustererBtn);
clustererChange.appendChild(inputSection);
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, MMapFeatureDataSource, MMapLayer, 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');
// Declare number of points in the clusterer
let pointsCount = 100;
map = new MMap(document.getElementById('app'), {location: LOCATION, showScaleInCopyrights: true});
// Create and add to the map a layer with a map schema, data sources, a layer of markers
map.addChild(new MMapDefaultSchemeLayer({}))
.addChild(new MMapFeatureDataSource({id: 'clusterer-source'}))
.addChild(new MMapLayer({source: 'clusterer-source', type: 'markers', zIndex: 1800}));
// You can set any markup for the marker and for the cluster
const contentPin = document.createElement('div');
contentPin.innerHTML = '<img src="../pin.svg" class="pin">';
/* 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 mappable.MMapMarker. */
const marker = (feature: Feature) =>
new MMapMarker(
{
coordinates: feature.geometry.coordinates,
source: 'clusterer-source'
},
contentPin.cloneNode(true) as HTMLElement
);
// 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,
source: 'clusterer-source'
},
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: getRandomPoints(pointsCount, BOUNDS),
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() {
clusterer.update({features: getRandomPoints(pointsCount, map.bounds)});
}
// The handler function for attach/detach the clusterer
function toggleClusterer() {
if (clusterer.parent) {
map.removeChild(clusterer);
} else {
map.addChild(clusterer);
}
}
// Creating and adding a custom clusterer change element to the map
map.addChild(
new MMapControls({position: 'bottom'}).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" />
</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="../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 {Feature} from '@mappable-world/mappable-clusterer';
import {getRandomPoints} 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,
MMapFeatureDataSource,
MMapLayer,
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 {useState, useMemo, useCallback} = React;
function App() {
// We declare the initial states of the clusterer
// Clusterer visibility
const [isClusterer, setIsClusterer] = useState(true);
// 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}), []);
// We declare a function for rendering ordinary markers. Note that the function must return any Entity element. In the example, this is mappable.MMapMarker
const marker = useCallback(
(feature: Feature) => (
<MMapMarker key={feature.id} coordinates={feature.geometry.coordinates} source="clusterer-source">
<img src="../pin.svg" className="pin" />
</MMapMarker>
),
[]
);
// 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 = useCallback(
(coordinates: LngLat, features: Feature[]) => (
<MMapMarker
key={`${features[0].id}-${features.length}`}
coordinates={coordinates}
source="clusterer-source"
>
<div className="circle">
<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: React.ChangeEvent<HTMLInputElement>) => setPointsCount(+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 location={LOCATION} showScaleInCopyrights={true} ref={(x) => (map = x)}>
{/* Add a map scheme layer */}
<MMapDefaultSchemeLayer />
{/* Add clusterer data sources */}
<MMapFeatureDataSource id="clusterer-source" />
{/* Add the layer for markers and the clusterer */}
<MMapLayer source="clusterer-source" type="markers" zIndex={1800} />
{/* 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} />
)}
{/* Add a custom clusterer change element to the map */}
<MMapControls position="bottom">
<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}
></input>
</div>
<div className="clusterer-change__section">
<button type="button" className="clusterer-change__btn" onClick={updatePoints}>
Update points
</button>
<button type="button" className="clusterer-change__btn" onClick={toggleClusterer}>
Delete/Add Clusterer
</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" />
</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>
<!-- 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="../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"></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>
.clusterer-change {
padding: 10px;
border-radius: 10px;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.clusterer-change__section {
display: flex;
align-items: center;
gap: 10px;
}
.clusterer-change__input__label {
font-size: 18px;
}
.clusterer-change__input {
max-width: 75px;
height: 100%;
font-size: 16px;
text-align: center;
border: 2px solid rgba(0, 122, 252, 0.6);
border-radius: 5px;
font-size: 16px;
outline: none;
transition: border-color 0.2s ease;
}
.clusterer-change__input:hover,
.clusterer-change__input:active {
border-color: rgba(0, 122, 252, 0.9);
}
.clusterer-change__btn {
border: none;
cursor: pointer;
padding: 7px 10px;
color: rgb(255, 255, 255);
font-size: 16px;
background-color: rgba(0, 122, 252, 0.9);
border-radius: 10px;
transition: background-color 0.2s;
}
.clusterer-change__btn:hover {
background-color: rgb(0, 110, 252);
}
.clusterer-change__btn:active {
background-color: rgb(0, 122, 252);
}
.circle {
position: relative;
width: 40px;
height: 40px;
color: #7234c3;
border: 2px solid currentColor;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.7);
transform: translate(-50%, -50%);
}
.circle-content {
position: absolute;
top: 50%;
left: 50%;
display: flex;
justify-content: center;
align-items: center;
width: 70%;
height: 70%;
border-radius: 50%;
background-color: currentColor;
transform: translate3d(-50%, -50%, 0);
}
.circle-text {
font-size: 0.9em;
color: #fff;
}
.pin {
transform: translate(-50%, -100%);
}
import type {LngLatBounds, LngLat} from '@mappable-world/mappable-types';
import type {Feature} from '@mappable-world/mappable-clusterer';
// 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)}
}));
};
mappable.ready.then(() => {
mappable.import.registerCdn('https://cdn.jsdelivr.net/npm/{package}', [
'@mappable-world/mappable-clusterer@latest'
]);
});
import type {MMapLocationRequest, LngLatBounds, LngLat} from '@mappable-world/mappable-types';
import type {Feature} from '@mappable-world/mappable-clusterer';
/* Rectangle bounded by bottom-left and top-right coordinates
Inside it, we generate the first bundle of clusterer points */
export const BOUNDS: LngLatBounds = [
[54.57971, 25.99359],
[56.29971, 24.47359]
];
export const LOCATION: MMapLocationRequest = {
bounds: BOUNDS as LngLatBounds, // starting position
zoom: 9 // starting zoom
};
{"node": 20, "template": "static"}