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 I can 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 the map so I wanted prototype it 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 to create photo clusters on the map.
(Thanks 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>