Display your assets on Woosmap Map using Marker Clustering

Displaying a large number of points on a map, keeping it clear and usable is a hard job, especially at higher level where identifying each feature is quite impossible.

Marker clustering is a technique where individual points are grouped into clusters according to a specific algorithm based on the radius of clusters. Grouped points are represented on the map by a marker, the cluster symbol, with the number of points included in it. Clustering eliminates overlapping points and makes the web map clearer.

Here is an image that shows how your web mapping application will look like implementing this representation method.

When zooming in, the clusters shrink, and individual points are displayed when distance exceed radius parameter.

Check this live example which displays all the pubs across the UK ~ 22.000 pubs :beer::

This sample implements the popular Supercluster library which uses the k-d trees data structure for spatial indexing. Having an index for each zoom allows to instantly query clusters for any map view and ensure smooth transitions between zoom levels.

Supercluster works pretty well for Woosmap Map JS. The library expect location data to be in the form of an array of GeoJSON Feature objects. Each feature’s geometry must be a GeoJSON Point.

First step is to load your GeoJSON file and build the spatial index as below:

fetch("https://demo.woosmap.com/misc/data/uk-all-pubs.geojson")
    .then((response) => response.json())
    .then((data) => {
      const geojsonFeatures = data["features"];
      index = new Supercluster({
        radius: 40,
        extent: 256,
        maxZoom: 14,
        minPoints: 5
      }).load(geojsonFeatures);
    });

Processing points is done only once. Clusters are computed for each zoom (between minZoom and maxZoom parameters).

To highlight processing time, you can pass log: true parameter to Supercluster constructor. For this sample, whole index is built in less than 50ms. Blazing fast!

prepare 22169 points: 1.5ms 
z14: 19530 clusters in 18ms
z13: 16822 clusters in 10ms
z12: 14141 clusters in 6ms
z11: 11050 clusters in 4ms
z10: 7121 clusters in 3ms
z9: 3101 clusters in 2ms
z8: 1020 clusters in 0ms
z7: 277 clusters in 0ms
z6: 86 clusters in 0ms
z5: 27 clusters in 0ms
z4: 9 clusters in 0ms
z3: 2 clusters in 0ms
z2: 1 clusters in 0ms
z1: 1 clusters in 0ms
z0: 1 clusters in 0ms
total time: 44.5ms 

Once built, clusters’ index can be displayed as a marker, labeled with the number of locations. Here the marker is a circle which radius and color vary depending on the cluster size.

const clusterData = index.getClusters(bbox, map.getZoom());
const markers = clusterData.map((feature) => {
  return createClusterIcon(feature)
});
function createClusterIcon(feature) {
    const count = feature.properties.point_count;
    const color = count < 80 ? "#0B9D58" : count < 500 ? "#F5B300" : "#DA0A40";
    const size = count < 80 ? 45 : count < 400 ? 55 : 65;
    const svg = window.btoa(`
        <svg fill="${color}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240">
          <circle cx="120" cy="120" opacity=".2" r="110" />
        </svg>`);
    const marker = new woosmap.map.Marker({
        labelOrigin: new woosmap.map.Point(13, 15),
        label: {
            text: feature.properties.point_count_abbreviated,
            color: "white"
        },
        position: latlng,
        icon: {
            url: `data:image/svg+xml;base64,${svg}`,
            scaledSize: new window.woosmap.map.Size(size, size)
        },
        map: map
    });
    return marker;
}

We’re looking forward to seeing cool clustering implementation with Woosmap Map from you! If you have any questions or comments don’t hesitate to react in this thread.

3 Likes

The above demo displays assets from a static GeoJSON file.
Of course, you can also use the ones hosted in your Woosmap project.

You’ll first need to retrieve all assets before passing them to Supercluster library.

Since the Woosmap Search API limits the number of assets fetched at one time, you need to call the API repeatedly to get all your stores. Each search request is limited to 300 assets as a maximum.

Here is a sample which implements recursive Promises in Javascript. Calls to Woosmap Search are done through Woosmap Map JS class woosmap.map.StoresService.

const storesService = new woosmap.map.StoresService();

const getAllStores = async () => {
    const allStores = [];
    const query = { storesByPage: 300 };
    
    const getStores = async (storePage) => {
      if (storePage) {
        query.page = storePage;
      }
      return storesService
        .search(query)
        .then((response) => {
          allStores.push(...response.features);
          if (query.page === response.pagination.pageCount) {
            return allStores;
          }
          return getStores(response.pagination.page + 1);
        })
        .catch((err) => {
          console.error(err);
          throw new Error("Error getting all stores");
        });
    };
    return getStores();
  };

  getAllStores().then((stores) => {
    index = new Supercluster({/*options*/}).load(stores);
  });

Details of the above code:

  1. Define the asynchronous function getAllStores() to fetch all the assets from Woosmap
  2. Define the asynchronous function, getStores(), to run the actual query on your project. It takes storePage as a parameter. This is the function that will be called recursively.
  3. Set the starting page number for the query.page based on the storePage and query.storesByPage to the max allowed number 300.
  4. Call the StoresService search function. This function returns a Javascript Promise.
  5. Add the fetched assets features to the allStores list.
  6. When all assets have been fetched (i.e storePage is equal to response.pagination.pageCount) resolve the Promise with allStores list as the value.
  7. As long as there are more assets to fetch, recursively call the getStores() function with the storePage as the input parameter.

The features returned from Woosmap Search API follow the GeoJSON standard so you can pass directly the results of getAllStores as a parameter to SuperCluster.load().

See full demo here: