0

I have a map project made on MaplibreGl.

It's a map with a list on the left, as in the mapbox example, clusters, and a filter based on radio buttons.

In this project, I'd like to be able to color cluster icons based on a property contained in my geoJson source. I managed to color the markers in my 'unclustered-point' layer.

I was able to consult a few examples on the subject, without managing to make them work:

changing cluster colors dynamically

Color Cluster based on GeoJSON Feature

Dynamic styling of cluster depending on a feature in it

These examples give me the impression that it is quite possible from a single source.

1- Is there an easy way to change the color of the clusters either by using the clusterProperties attribute in the map.addSource function or by using the paint and circle-color attribute in the "cluster" layer?

2-If it is preferable to go through several sources, then how do I manage the listing and filters?

Should we also duplicate the layers ('cluster', 'cluster count' and 'unclustered-point')?

Is it possible to do something like setData(stores1,store2 etc); map.getSource('stores1','stores2','stores3') in a filtering function?

Here is a working example with a single source. It was made largely with the help of TomazicM, especially for the filters. So my idea here would be to succeed in coloring the cluster icons: region 1 in red and region 2 in green. The default color of the other clusters would be blue (here region 3)

I tried to create the three sources. The problem is that I duplicate the layers, so instead of having 3, I end up with 9 layers. And the same goes for the filter functions and the listing, as they are based on a single source, I tried to duplicate them... And that's where it all gets complicated...

 <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <title>Add a raster image to a map layer</title>
      <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
      <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
      <link href="https://unpkg.com/[email protected]/dist/maplibre-gl.css" rel="stylesheet">
      <script src="https://unpkg.com/[email protected]/dist/maplibre-gl.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.0/jquery.min.js"></script>
      <script src="https://cdn.jsdelivr.net/npm/@turf/turf@7/turf.min.js"></script>
      <style>
        body { margin: 0; padding: 0; }
        html, body, #map { height: 100%; }
        .sidebar {
          position:absolute;
          top:0;
          left:0;
          width:25%;
          background:white;
        }
        #map{
          position:absolute;
          top:0;
          left:25%;
          width:75%
        }
        .top {
           position: absolute;
           top: 0;
           right: 0;
           background:white;
        }
      </style>
    </head>
    <body>
    <div style="padding:100px 0"><h1>My title</h1></div>
        <div style="position:relative;height:100%">
          <div class="sidebar">
            <div class="heading font-body text-title-4 font-title text-primary1">
            <p>Hospitals</p>
            </div>
           <div id="listings" class="listings font-body"></div>
          </div>
          
        <div id="map" class="map pad2"></div> 
        <div class="top">
            <div class="map-overlay-inner">
                <nav id="nav-filter">
                     <fieldset>
                        <legend>Région</legend>
                        <div>
                            <input id="all" type="radio" name="radio" onclick="filterMarkers()"/>
                            <label for="all">All</label>
                        </div>
    
                        <div>
                            <input id="region1" type="radio" name="radio" onclick="filterMarkers(this)"/>
                            <label for="region1">region 1</label>
                        </div>
                        <div>
                            <input id="region2" type="radio" name="radio" onclick="filterMarkers(this)"/>
                            <label for="region2">region 2</label>
                        </div>
                        <div>
                            <input id="region3" type="radio" name="radio" onclick="filterMarkers(this)"/>
                            <label for="region3">region 3</label>
                        </div>
                    </fieldset>
                </nav>
                
            </div>
        </div>      
        </div>
        <div style="padding:100px 0"><h2>My footer</h2></div>
    <script>
      var map = new maplibregl.Map({
        container: 'map', // container id
        style: {
          'version': 8,
          'sources': {
            'raster-tiles': {
              'type': 'raster',
              'tiles': [
                "https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png",
                "https://b.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png",
                "https://c.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png",
                "https://d.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png"
              ],
              'tileSize': 256,
              'attribution':
              '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'
            }
          },
           "glyphs": "https://fonts.openmaptiles.org/{fontstack}/{range}.pbf",
          'layers': [
            {
            'id': 'simple-tiles',
            'type': 'raster',
            'source': 'raster-tiles',
            'minzoom': 0,
            'maxzoom': 22
            }
          ]
        },
        center: [2.3514998, 48.85661], // starting position
        zoom: 5, // starting zoom
        cooperativeGestures: true
      });
        
       var stores = 
       {
        "type": "FeatureCollection",
        "features": [
           {
          "type": "Feature",
          "geometry": {
            "type": "Point",
            "coordinates": [
              5.8600224,
              48.6805776
            ]
          },
          "properties": {
            "name": "house 3",
            "region": "region 2",
          }
        },
        {
          "type": "Feature",
          "geometry": {
            "type": "Point",
            "coordinates": [
              6.099311399999999,
              48.6143564
            ]
          },
          "properties": {
            "name": "house 2",
            "region": "region 2",
          }
        },
        {
          "type": "Feature",
          "geometry": {
            "type": "Point",
            "coordinates": [
              6.1611808,
              48.6718265
            ]
          },
          "properties": {
            "name": "house1",
            "region": "region 2",
           
          }
        },
         {
          "type": "Feature",
          "geometry": {
            "type": "Point",
            "coordinates": [
              -0.5193906,
              45.9452769
            ]
          },
          "properties": {
            "name": "house 6",
            "region": "region 1"
          }
        },
        {
          "type": "Feature",
          "geometry": {
            "type": "Point",
            "coordinates": [
              -1.0026331,
              45.62014
            ]
          },
          "properties": {
    
            "name": "house 5",
            "region": "region 1"
          }
        },
        {
          "type": "Feature",
          "geometry": {
            "type": "Point",
            "coordinates": [
              0.1955502,
              46.0287897
            ]
          },
          "properties": {
            "name": "house 4",
            "region": "region 1"
          }
        },
        {
          "type": "Feature",
          "geometry": {
            "type": "Point",
            "coordinates": [
              3.689009,
              43.4474489
            ]
          },
          "properties": {
            "name": "house 9",
            "region": "region 3"
          }
        },
        {
          "type": "Feature",
          "geometry": {
            "type": "Point",
            "coordinates": [
              3.9077075,
              43.62659379999999
            ]
          },
          "properties": {
            "name": "house 8",
            "region": "region 3"
          }
        },
    
    
        {
          "type": "Feature",
          "geometry": {
            "type": "Point",
            "coordinates": [
              3.8874525,
              43.5972365
            ]
          },
          "properties": {
            "name": "house 7",
            "region": "region 3"
          }
        }
          ]
      };
      
      stores.features.forEach((store, i) => {
        store.properties.id = i;
      });
    
      function getLabel(id) {
        return $("#"+id).next("label").html();
      }
      
      var regions = {};
      
      const regionCheckboxes = document.querySelectorAll('input[type=radio]');
      regionCheckboxes.forEach(checkbox => {
        const region = getLabel(checkbox.id);
        if (region != 'All') {
          var filteredFeatures = stores.features.filter(feature => (feature.properties.region == region));
          var bbox = turf.bbox(turf.featureCollection(filteredFeatures));
          
          regions[checkbox.id] = bbox;
        }
      });
             
      map.on('load', () => {
        map.addSource('stores', {
          'type': 'geojson',
          'data': stores,
          'cluster': true,
          'clusterMaxZoom': 14, // Max zoom to cluster points on
          'clusterRadius': 50
        });
    
        // addMarkers();
        map.addLayer({
          id: 'clusters',
          type: 'circle',
          source: 'stores',
          filter: ['has', 'point_count'],
          paint: {
            'circle-color': [
              'step',
              ['get', 'point_count'],
              '#525CA3',
              100,
              '#525CA3',
              750,
              '#525CA3'
            ],
            'circle-radius': [
              'step',
              ['get', 'point_count'],
              20,
              100,
              30,
              750,
              40
            ]
          }
        });
    
        map.addLayer({
          id: 'cluster-count',
          type: 'symbol',
          source: 'stores',
          filter: ['has', 'point_count'],
          layout: {
            'text-field': '{point_count_abbreviated}',
            'text-font': ['Open Sans Bold'],
           
            'text-size': 12,
            "icon-image": "{marker-symbol}",
            "icon-size": 0.20
          },
          paint: {
            "text-color": "#ffffff"
          }
        });
    
    
        map.addLayer({
          id: 'unclustered-point',
          type: 'circle',
          source: 'stores',
          filter: ['!', ['has', 'point_count']],
          paint: {
            'circle-color': '#525CA3',
            'circle-radius': 15,
            'circle-stroke-width': 1,
            'circle-stroke-color': '#fff'
          }
        });
    
        buildLocationList(stores); 
    
        map.on('click', 'clusters', (e) => {
          const features = map.queryRenderedFeatures(e.point, {
            layers: ['clusters']
          });
          const clusterId = features[0].properties.cluster_id;
          map.getSource('stores').getClusterExpansionZoom(
            clusterId,
            (err, zoom) => {
              if (err) return;            
              map.easeTo({
                center: features[0].geometry.coordinates,
                zoom: zoom
              });
            }
          );
        });
    
        map.on('click', 'unclustered-point', (e) => {
          const coordinates = e.features[0].geometry.coordinates.slice();
          const region = e.features[0].properties.region;
          const name = e.features[0].properties.name;
          
          while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
            coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
          }
           
          for (const marker of stores.features) {
            const activeItem = document.getElementsByClassName('active');
            if (activeItem[0]) {
              activeItem[0].classList.remove('active');
            }
            const listing = document.getElementById(
              `listing-${e.features[0].properties.id}`
            );
            listing.classList.add('active');
            listing.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' });
          };
          
          new maplibregl.Popup({ closeOnClick: true })
          .setLngLat(coordinates)
          .setHTML(`<h3>${listing.properties.name}</h3><h4>${listing.properties.region}</h4>`)
          .addTo(map);
        });
                   
        map.on('mouseenter', 'clusters', () => {
          map.getCanvas().style.cursor = 'pointer';
        });
        map.on('mouseleave', 'clusters', () => {
          map.getCanvas().style.cursor = '';
        });
      });
        
          
      function buildLocationList(stores) {
        for (const store of stores.features) {
          const listings = document.getElementById('listings');
          const listing = listings.appendChild(document.createElement('div'));
          listing.id = `listing-${store.properties.id}`;
          listing.className = 'item';
          const link = listing.appendChild(document.createElement('a'));
          link.href = 'javascript:void(0)';
          link.className = 'title';
          link.id = `link-${store.properties.id}`;
          link.innerHTML = `<h3 style="margin:0 0 10px">${store.properties.name}</h3>`;
          const details = listing.appendChild(document.createElement('div'));
        
          if(store.properties.region) {
            details.innerHTML +=  `<p style="margin:0">${store.properties.region }</p>`
          }
          link.addEventListener('click', function () {
            for (const feature of stores.features) {
              if (this.id === `link-${feature.properties.id}`) {
                flyToStore(feature);
                createPopUp(feature);
              }
            }
            const activeItem = document.getElementsByClassName('active');
            if (activeItem[0]) {
              activeItem[0].classList.remove('active');
            }
              this.parentNode.classList.add('active');
    
          });
        }
      }  
      
      function flyToStore(currentFeature) {
        map.flyTo({
          center: currentFeature.geometry.coordinates,
          zoom: 15
        });
      }
      
      function createPopUp(currentFeature) {        
        const popUps = document.getElementsByClassName('maplibregl-popup');
        if (popUps[0]) popUps[0].remove();
        const popup = new maplibregl.Popup({ closeOnClick: true })
          .setLngLat(currentFeature.geometry.coordinates)
          .setHTML(
            `'<h3>${currentFeature.properties.name}</h3><h4>${currentFeature.properties.region}</h4>`
          )  
          .addTo(map);   
      }           
      
      const nav = new maplibregl.NavigationControl({
        visualizePitch: true
      });
      map.addControl(nav, 'bottom-right');
        
      function getSelectedRegions() {
        const selectedRegions = [];
        const regionCheckboxes = document.querySelectorAll('input[type=radio]:checked');
        regionCheckboxes.forEach(checkbox => selectedRegions.push(getLabel(checkbox.id)));
        return selectedRegions;
      }
       
      function filterMarkers(button) {
        const selectedRegions = getSelectedRegions();
        const mapSource = map.getSource('stores');
        if (selectedRegions[0] == 'All') {
          setAllMarkersVisible(mapSource);
          return;
        }
        const filteredFeatures = stores.features.filter(feature => selectedRegions.includes(feature.properties.region));
        mapSource.setData({
          ...stores,
          features: filteredFeatures
        });
        map.fitBounds(regions[button.id],  {
          padding: {top: 50, bottom: 50, left: 50, right: 50}
        });
      }
       
      function setAllMarkersVisible(mapSource) {
        mapSource.setData(stores);
        map.flyTo({
          center: [2.3514998, 48.85661], // starting position
          zoom: 5, // starting zoom
        });
      }
    </script>
     
    </body>
    </html>

0

1 Answer 1

2

To make clusters for each region independent of markers from neighboring regions, as many sources as there are regions have to be created. Then there is also no problem coloring them differently. Below is described one possible way how to do it.

First object region is defined with colors for all regions. It will be also used to store bounding boxes for regions:

const regions = {
  region1: {color: '#FF0000'},
  region2: {color: '#00FF00'},
  region3: {color: '#0000FF'},
};

Then regions are iterated and separate source, layers are created for each region, using region color for markers and clusters, and bounding boxes for regions are calculated:

map.on('load', () => {
  const regionCheckboxes = document.querySelectorAll('input[type=radio]');
  regionCheckboxes.forEach(checkbox => {
    var region = getLabel(checkbox.id);
    if (region != 'All') {
      var filteredFeatures = stores.features.filter(feature => (feature.properties.region == region));
      var filteredStores = turf.featureCollection(filteredFeatures);
      var bbox = turf.bbox(filteredStores);
      
      regions[checkbox.id].bbox = bbox;
      
      var storesId = 'stores_' + checkbox.id;
      var clustersId = 'clusters_' + checkbox.id;
      var clusterCountId = 'cluster-count_' + checkbox.id;
      var unclusteredPointId = 'unclustered-point_' + checkbox.id;

      map.addSource(storesId, {
        'type': 'geojson',
        'data': filteredStores,
        'cluster': true,
        'clusterMaxZoom': 14,
        'clusterRadius': 50
      });

      map.addLayer({
        id: clustersId,
        type: 'circle',
        source: storesId,
        filter: ['has', 'point_count'],
        paint: {
          'circle-color': [
            'step',
            ['get', 'point_count'],
            regions[checkbox.id].color,
            100,
            regions[checkbox.id].color,
            750,
            regions[checkbox.id].color
          ],
          'circle-radius': [
            'step',
            ['get', 'point_count'],
            20,
            100,
            30,
            750,
            40
          ]
        }
      });
  
      map.addLayer({
        id: clusterCountId,
        type: 'symbol',
        source: storesId,
        filter: ['has', 'point_count'],
        layout: {
          'text-field': '{point_count_abbreviated}',
          'text-font': ['Open Sans Bold'],
         
          'text-size': 12,
          "icon-image": "{marker-symbol}",
          "icon-size": 0.20
        },
        paint: {
          "text-color": "#ffffff"
        }
      });
      
      map.addLayer({
        id: unclusteredPointId,
        type: 'circle',
        source: storesId,
        filter: ['!', ['has', 'point_count']],
        paint: {
          'circle-color': regions[checkbox.id].color,
          'circle-radius': 15,
          'circle-stroke-width': 1,
          'circle-stroke-color': '#fff'
        }
      });
    }

    map.on('click', clustersId, (e) => {
      const features = map.queryRenderedFeatures(e.point, {
        layers: [clustersId]
      });
      const clusterId = features[0].properties.cluster_id;
      map.getSource(storesId).getClusterExpansionZoom(
        clusterId,
        (err, zoom) => {
          if (err) return;            
          map.easeTo({
            center: features[0].geometry.coordinates,
            zoom: zoom
          });
        }
      );
    });

    map.on('click', unclusteredPointId, (e) => {
      const feature = e.features[0];
      const coordinates = feature.geometry.coordinates.slice();
      const region = feature.properties.region;
      const name = feature.properties.name;
      
      while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
        coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
      }
       
      for (const marker of stores.features) {
        const activeItem = document.getElementsByClassName('active');
        if (activeItem[0]) {
          activeItem[0].classList.remove('active');
        }
        const listing = document.getElementById(
          `listing-${e.features[0].properties.id}`
        );
        listing.classList.add('active');
        listing.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' });
      };
      
      new maplibregl.Popup({ closeOnClick: true })
      .setLngLat(coordinates)
      .setHTML(`<h3>${feature.properties.name}</h3><h4>${feature.properties.region}</h4>`)
      .addTo(map);
    }); 

    map.on('mouseenter', clustersId, () => {
      map.getCanvas().style.cursor = 'pointer';
    });
    map.on('mouseleave', clustersId, () => {
      map.getCanvas().style.cursor = '';
    });
  });       
});

In filterMarkers function which shows only selected region, all other regions are hidden and map.fitBounds called for bounding box of selected region:

function filterMarkers(button) {
  const selectedRegions = getSelectedRegions();
  if (selectedRegions[0] == 'All') {
    setAllMarkersVisible();
    return;
  }
  for (var regionId in regions) {
    if (regionId != button.id) {
      map.setLayoutProperty('clusters_' + regionId, 'visibility', 'none');
      map.setLayoutProperty('unclustered-point_' + regionId, 'visibility', 'none');
    }
  }
  map.setLayoutProperty('clusters_' + button.id, 'visibility', 'visible');
  map.setLayoutProperty('unclustered-point_' + button.id, 'visibility', 'visible');
  map.fitBounds(regions[button.id].bbox,  {
    padding: {top: 50, bottom: 50, left: 50, right: 50}
  });
}

In setAllMarkersVisible which resets to initial view, all regions are set visible and map.flyTo called to go to initial zoom and position:

function setAllMarkersVisible() {
  for (var regionId in regions) {
    map.setLayoutProperty('clusters_' + regionId, 'visibility', 'visible');
  }
  map.flyTo({
    center: [2.3514998, 48.85661], // starting position
    zoom: 5, // starting zoom
  });
}

Here is working JSFiddle (header and footer sections are hidden, since they are not relevant to the question): https://jsfiddle.net/TomazicM/2bhtdngc/

0

Not the answer you're looking for? Browse other questions tagged or ask your own question.