Photo clusters with mapbox-gl-js

I've seen photo clusters on map in the past, think way back Instagram used to have it, so it's nothing new. But I wanted to see if it was possible to create them using mapbox-gl-js as I've been using it quite extensively at work and thought there might be a product fit possibility for photo clusters on a page with a big map 😉 so wanted to prototype a bit to get a better understanding what is possible and how it might be done.
While Mapbox GL JS documentation has lot's of good examples ✨, they do not have an example on how to do photo clusters. However after finding this two examples it was clear that it should be possible 🤩
Thank you Mapbox team for having awesome docs and examples 🙌🏻
Interactive Example
Code Example
Here is the code that follows similar pattern as on Mapbox GL JS example pages:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Display Photo clusters</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<link href="https://api.mapbox.com/mapbox-gl-js/v1.13.0/mapbox-gl.css" rel="stylesheet" />
<script src="https://api.mapbox.com/mapbox-gl-js/v1.13.0/mapbox-gl.js"></script>
<style>
body { margin: 0; padding: 0; }
#map { bottom: 0; position: absolute; top: 0; width: 100%; }
.cluster-base {
background-color: #fff;
border: 4px solid #fff;
border-radius: 4px;
box-shadow: 0 3px 3px rgba(0, 0, 0, 0.1);
position: relative;
}
.cluster-base img {
border-radius: 4px;
display: flex;
height: 60px;
object-fit: cover;
width: 60px;
}
.cluster-base .count {
background-color: #f00;
border-radius: 100px;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);
color: #fff;
padding: 0 6px;
position: absolute;
right: -10px;
top: -10px;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
// TO MAKE THE MAP APPEAR YOU MUST
// ADD YOUR ACCESS TOKEN FROM
// https://account.mapbox.com
mapboxgl.accessToken = '<your access token here>';
var map = new mapboxgl.Map({
container: 'map',
zoom: 11,
center: [-122.4194, 37.7749],
style: 'mapbox://styles/mapbox/light-v10',
});
map.addControl(new mapboxgl.NavigationControl());
map.on('load', function() {
map.addSource('photo-cluster', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [{
type: 'Feature',
properties: { src: 'http://placekitten.com/g/400/400' },
geometry: { type: 'Point', coordinates: [-122.462, 37.782] }
}, {
type: 'Feature',
properties: { src: 'http://placekitten.com/500/500' },
geometry: { type: 'Point', coordinates: [-122.462, 37.785] }
}, {
type: 'Feature',
properties: { src: 'http://placekitten.com/300/300' },
geometry: { type: 'Point', coordinates: [-122.468, 37.782] }
}, {
type: 'Feature',
properties: { src: 'http://placekitten.com/350/350' },
geometry: { type: 'Point', coordinates: [-122.47, 37.79] }
}]
},
cluster: true,
clusterRadius: 64,
clusterProperties: {
// might want to set a default value here
src: ['string', ['get', 'src'], '']
}
});
map.addLayer({
id: 'photo-clusters',
type: 'circle',
source: 'photo-cluster',
filter: ['!=', 'cluster', true],
paint: {
// this will hide singular cluster
'circle-color': 'rgba(0, 0, 0, 0)',
'circle-radius': 64
}
});
// objects for caching and keeping track of HTML
// marker objects (for performance)
var markers = {};
var markersOnScreen = {};
function updateMarkers() {
var newMarkers = {};
var features = map.querySourceFeatures('photo-cluster');
// for every cluster on the screen, create an HTML marker for it
// (if we didn't yet), and add it to the map if it's not there already
for (var i = 0; i < features.length; i++) {
var coords = features[i].geometry.coordinates;
var props = features[i].properties;
// when it's a cluster there will be using cluster_id,
// otherwise we can grab src as it should be unique
var markerId = props.cluster ? props.cluster_id : props.src;
var marker = markers[markerId];
// if marker is not present create it
if (!marker) {
var el = createClusterElement(props);
markers[markerId] = new mapboxgl.Marker({
element: el
}).setLngLat(coords);
marker = markers[markerId];
}
newMarkers[markerId] = marker;
if (!markersOnScreen[markerId]) marker.addTo(map);
}
// for every marker we've added previously, remove those that are no
// longer visible
for (id in markersOnScreen) {
if (!newMarkers[id]) markersOnScreen[id].remove();
}
markersOnScreen = newMarkers;
}
// update markers on the screen on every frame
map.on('render', function() {
updateMarkers();
});
});
function createClusterElement(props) {
var html = '<div class="cluster-base">' +
(props.cluster
? '<span class="count">'+ props.point_count + '</span>'
: ''
) +
'<img src="'+ props.src +'" />' +
'</div>';
var el = document.createElement('div');
el.innerHTML = html;
return el;
}
</script>
</body>
</html>