Back to all examples

Places clustering playground

This example on GitHub

Tune how charging and fuel stations cluster, how the count badge looks and what it counts

Places clustering playground
import type { Place } from '@tomtom-org/maps-sdk/core';
import { TomTomConfig } from '@tomtom-org/maps-sdk/core';
import { calculatePaddedBBox, PlacesModule, TomTomMap } from '@tomtom-org/maps-sdk/map';
import { createLatestRequest, discoverPlaces } from '@tomtom-org/maps-sdk/services';
import type { GeoJSONSource } from 'maplibre-gl';
import './style.css';
import { API_KEY } from './config';
import { CONNECTORS_PROPERTY, initPanel, readClusterConfig, showZoom } from './panel';
import { initTogglePanel } from './togglePanel';

// (Set your own API key when working in your own environment)
TomTomConfig.instance.put({ apiKey: API_KEY, language: 'en-GB' });

const countConnectors = (place: Place): number =>
    place.properties.chargingPark?.connectors.reduce((total, { count }) => total + count, 0) ?? 0;

(async () => {
    const map = new TomTomMap({
        mapLibre: {
            container: 'sdk-map',
            center: [4.8897, 52.3667],
            zoom: 12,
        },
        style: 'monoLight',
    });

    const places = await PlacesModule.create(map, {
        markerType: 'pin-clustered',
        // Every place carries its connector count, which the cluster source sums per cluster.
        extraFeatureProps: { [CONNECTORS_PROPERTY]: countConnectors },
        ...readClusterConfig(),
    });

    // A cluster isn't a place, so the module-wide events receive it, as MapLibre renders it.
    places.events
        .where((feature) => feature.properties.cluster === true)
        .on('click', async (cluster, lngLat) => {
            const source = map.mapLibreMap.getSource<GeoJSONSource>(cluster.source);
            const zoom = await source?.getClusterExpansionZoom(cluster.properties.cluster_id);
            map.mapLibreMap.easeTo({ center: lngLat, zoom });
        });

    // A newer search retires the one in flight, so a slow response cannot repopulate the map.
    const latestSearch = createLatestRequest();
    const searchView = async () => {
        // Null when the panel leaves too little of the map to search in.
        const boundingBox = calculatePaddedBBox({ map, surroundingElements: ['.ui-customPanel'] });
        if (!boundingBox) return;

        const results = await latestSearch.run((signal) =>
            Promise.all(
                (['CHARGING_LOCATION', 'FUEL_STATION'] as const).map((category) =>
                    discoverPlaces({
                        filters: { poiCategories: [category] },
                        geoBias: { boundingBox },
                        limit: 100,
                        signal,
                    }),
                ),
            ),
        );
        if (results.current) await places.show(results.value.flatMap(({ features }) => features));
    };

    initTogglePanel();
    initPanel((config) => {
        places.updateConfig(config);
        showZoom(map.mapLibreMap.getZoom());
    });

    map.mapLibreMap.on('zoom', () => showZoom(map.mapLibreMap.getZoom()));
    map.mapLibreMap.on('moveend', searchView);
    showZoom(map.mapLibreMap.getZoom());
    await searchView();
})();

Nearby stations merge into counted clusters while zoomed out, a click on one zooms in until it splits, and every panel change redraws them on the places already shown.

  • The pin-clustered marker type of PlacesModule sorts clusters for you. A cluster of one category keeps its icon with a corner badge, and a mixed one gets a centred count.
  • A cluster isn’t a place, so it reaches the module-wide events, where events.where picks it out by its cluster property.
  • cluster sets the clustering options and the badge look in one config, instead of four layer overrides. Its source.properties sums a number onto each cluster for the badge to print, which extraFeatureProps attaches to each place.
  • The panel starts at the defaults that placesKnobCatalogue publishes, which you can read with no map loaded.

Related examples