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.
When you load the below code, you'll get a globe which you can spin and zoom. Nifty!
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:
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:
We want to turn it into this beauty:
Here's the code, adapted from the tutorial:
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.