Minimum Viable Clustered-Marker Globe using OpenFreeMap and MapLibre GL


I love OpenFreeMap it is a quick, easy, and free way to add beautiful maps to your Open Source projects. With the latest release of MapLibre-GL I wanted to see if there was an easy way to use both to make an interactive globe with clustered markers.

Spoiler alert: yes!

Basic Globe

Here's a basic example which I've trimmed down from this example.

A globe of the planet Earth with labels for countries.

When you load the below code, you'll get a globe which you can spin and zoom. Nifty!

HTML HTML<!DOCTYPE html>
<html>
    <head>
        <title>Globe Projection using OpenFreeMap</title>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="stylesheet" href="https://unpkg.com/maplibre-gl@5.0.0/dist/maplibre-gl.css">
        <script src="https://unpkg.com/maplibre-gl@5.0.0/dist/maplibre-gl.js"></script>
        <style>
            body { margin: 0;padding: 0; }
            html, body, #map { height: 100%; }
        </style>
    </head>
    <body>
        <div id="map"></div>
        <script type="module">
            const map = new maplibregl.Map({
                container: "map",
                style: "https://tiles.openfreemap.org/styles/liberty",
                zoom: 2,
                center: [0.123, 51.2345],
                pitch: 0,
                canvasContextAttributes: { antialias: true }
            });

            map.on('style.load', () => {
                map.setProjection({
                    type: 'globe',
                });
            });
        </script>
    </body>
</html>

Adding Markers

In most respects, this acts like a normal MapLibre GL map. To add a marker, add this code:

JavaScript JavaScriptconst marker = new maplibregl.Marker()
    .setLngLat([12.345, 54.321])
    .addTo(map);

GeoJSON Clusters

Now for the big one! On OpenBenches we display markers and clusters of markers. On a flat map, it looks like this:

Map of Europe. There are big circles on it saying how many markers are inside.

We want to turn it into this beauty:

Here's the code, adapted from the tutorial:

JavaScript JavaScriptconst map = new maplibregl.Map({
    container: "map",
    style: "https://tiles.openfreemap.org/styles/liberty",
    zoom: 2,
    center: [0.123, 51.2345],
    pitch: 0,
    canvasContextAttributes: { antialias: true }
});

map.on('style.load', () => {
    map.setProjection({
        type: 'globe',
    });
});

//  Load GeoJSON
async function load_GeoJSON() {
    const response = await fetch( "geo.json" )
    var benches_json = await response.json();
    return benches_json;
}

//  Asynchronous function to add custom layers and sources
async function addCustomLayersAndSources() {
    //  Get the data
    var geo_data = await load_GeoJSON();
    //  Load the GeoJSON
    if (!map.getSource('geo_data')) {
        map.addSource('geo_data', {
            type: 'geojson',
            data: geo_data,
            cluster: true,
            clusterMaxZoom: 17, // Max zoom to cluster points on
            clusterRadius: 50 // Radius of each cluster when clustering points (defaults to 50)
        });
    }

    //  Custom point marker
    if ( map.listImages().includes("marker-icon") == false ) {
        var image = await map.loadImage('/marker.png');
        map.addImage('marker-icon', image.data);
    }

    //  Add the clusters
    if (!map.getLayer('clusters')) {
        map.addLayer({
            id: 'clusters',
            type: 'circle',
            source: 'geo_data',
            filter: ['has', 'point_count'],
            paint: {
                // Use step expressions (https://maplibre.org/maplibre-style-spec/expressions/#step)
                // with three steps to implement three types of circles:
                //   * Blue, 20px circles when point count is less than 100
                //   * Yellow, 30px circles when point count is between 100 and 750
                //   * Pink, 40px circles when point count is greater than or equal to 750
                'circle-color': [
                    'step', ['get', 'point_count'],
                        '#51bbd655',
                    100, '#f1f07555',
                    750, '#f28cb155'
                ],
                'circle-radius': [
                    'step', ['get', 'point_count'],
                        20,
                    100, 30,
                    750, 40
                ],
                'circle-stroke-width': [
                    'step', ['get', 'point_count'],
                        1,
                    100, 1,
                    750, 1
                ],
                'circle-stroke-color': [
                    'step', ['get', 'point_count'],
                        '#000',
                    100, '#000',
                    750, '#000'
                ],
            }
        });

        //  Show number of markers in each cluster
        map.addLayer({
            id: 'cluster-count',
            type: 'symbol',
            source: 'geo_data',
            filter: ['has', 'point_count'],
            layout: {
                'text-field': '{point_count_abbreviated}',
                'text-font': ['Noto Sans Regular'],
                'text-size': 25
            }
        });

        //  Show individual markers
        map.addLayer({
            id: 'unclustered-point',
            source: 'geo_data',
            filter: ['!', ['has', 'point_count']],
            type: 'symbol',
            layout: {
                "icon-overlap": "always",
                'icon-image': 'marker-icon',  // Use the PNG image
                'icon-size': .1              // Adjust size if necessary
            }
        });
    }
}

//  Start by drawing the map
map.on('load', async () => {
    await addCustomLayersAndSources();
});

Obviously, there's a lot more you can do - but I hope this shows just how quickly you can get a clustermap working on a globe.


Share this post on…

  • Mastodon
  • Facebook
  • LinkedIn
  • BlueSky
  • Threads
  • Reddit
  • HackerNews
  • Lobsters
  • WhatsApp
  • Telegram

What are your reckons?

All comments are moderated and may not be published immediately. Your email address will not be published.

Allowed HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <p> <pre> <br> <img src="" alt="" title="" srcset="">