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

One thought on “Minimum Viable Clustered-Marker Globe using OpenFreeMap and MapLibre GL”

Trackbacks and Pingbacks

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="">