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.
<! 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 >