Photo clusters with mapbox-gl-js

Photo clusters on map

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>

© 2024 marexandre :]