1

I have an interactive map in which I display several hundred points.

The map is based on the tutorial "stores locator" and "Create and style clusters" from Mapbox or MapLibreGl.

I managed to create filters to filter by feature, in this case, the region. When a region is clicked, the corresponding markers are displayed. It works well. I first thought of checkboxes, but I think that complicates the use cases. So this time I'm trying with radio buttons.

Here is a sample of my code:

    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": [
              3.2803257,
              45.5400916
            ]
          },
          "properties": {
            "category": "Maisons sports et santé",
            "name": "Sport Santé Plus",
            "region": "Region one",
            "address": "16 Route d'Issoire, 63500, Parentignat",
            "phone": "Tel : 06 88 91 08 49",
            "email": "email : [email protected]",
            "website": "<a target='_blank' href='https://url.de.m.mimecastprotect.com/s/Z3NrCoZ34XsZ4V2vWF1fgCrWPXS?domain=sportsanteplus.com'>Site web</a>"
          }
        },
        {
          "type": "Feature",
          "geometry": {
            "type": "Point",
            "coordinates": [
              3.2803257,
              45.5400916
            ]
          },
     {
          "type": "Feature",
          "geometry": {
            "type": "Point",
            "coordinates": [
              5.4919297,
              47.079275
            ]
          },
          "properties": {
            "category": "Maison sport et santé",
            "name": "Maison Sport Santé Sociale Grad Dole",
            "region": "Region two",
            "address": "65 Rue de Crissey 39100 Dole",
            "phone": "Tel : 07 82 09 22 73",
            "email": "email : [email protected]",
            "website": ""
          }
        }
        ]
    };
    
     stores.features.forEach((store, i) => {
        store.properties.id = i;
      });
      
       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 category = e.features[0].properties.category;
                const region = e.features[0].properties.region;
                const name = e.features[0].properties.name;
                const address = e.features[0].properties.address;
                const phone = e.features[0].properties.phone;
                const email = e.features[0].properties.email;
                const website = e.features[0].properties.website;
              
                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');
                  // e.stopPropagation();
                  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: 'nearest', inline: 'start' });
                };
             
                new maplibregl.Popup({ closeOnClick: true })
                .setLngLat(coordinates)
                .setHTML(`<h3>${category}</h3><h4>${name}</h4><p style="margin:0">${region}</p><p style="margin:0">${address}</p><p style="margin:0">${phone}</p><p style="margin:0">${email}</p><p style="margin:0">${website}</p>`)
                .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.category) {
                details.innerHTML +=  `<h4 style="margin:0 0 10px">${store.properties.category }</h4>`
              }
              if(store.properties.region) {
                details.innerHTML +=  `<p style="margin:0">${store.properties.region }</p>`
              }
              if(store.properties.address) {
                details.innerHTML +=  `<p style="margin:0">${store.properties.address }</p>`
              }
              if(store.properties.phone) {
                details.innerHTML +=  `<p style="margin:0">${store.properties.phone }</p>`
              }
              if(store.properties.email) {
                details.innerHTML +=  `<p style="margin:0">${store.properties.email }</p>`
              }
               if(store.properties.website) {
                details.innerHTML +=  `<p style="margin:0">${store.properties.website }</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>${category}</h3><h4>${name}</h4><p style="margin:0">${region}</p><p style="margin:0">${address}</p><p style="margin:0">${phone}</p><p style="margin:0">${email}</p><p style="margin:0">${website}</p>`
                )
      
                .addTo(map);
      
            }
      
      const nav = new maplibregl.NavigationControl({
      visualizePitch: true
      });
      map.addControl(nav, 'bottom-right');
    function getLabel(id) {
      return $("#"+id).next("label").html();
    }
    function getSelectedRegions() {
    
      const selectedRegions = [];
      const regionRadios = document.querySelectorAll('input[type=radio]:checked');
      regionRadios.forEach(radio => selectedRegions.push(getLabel(radio.id)));
      return selectedRegions;
    }
     
    function filterMarkers() {
      const selectedRegions = getSelectedRegions();
      const mapSource = map.getSource('stores');
      if (selectedRegions.length === 0) {
        setAllMarkersVisible(mapSource); // Ensure to implement this function
        return;
      }
      mapSource.setData({
        ...stores,
        features: stores.features.filter(feature => selectedRegions.includes(feature.properties.region))
      });
    }
     
    function setAllMarkersVisible(mapSource) {
      mapSource.setData(stores);
    }

And in my html :

        <div>
          <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="map-overlay top">
            <div class="map-overlay-inner">
                <nav id="nav-filter">
                     <fieldset>
                        <legend>Région</legend>
    
    <div>
                            <input id="all" type="radio" name="radio"/>
                            <label for="all">All</label>
                        </div>
    
    <div>
                            <input id="region1" type="radio" name="radio" onclick="filterMarkers()"/>
                            <label for="region1">Region one</label>
                        </div>
                        <div>
                            <input id="region2" type="radio" name="radio" onclick="filterMarkers()"/>
                            <label for="region2">Region two</label>
                        </div>
                        <div>
                            <input id="region3" type="radio" onclick="filterMarkers()"/>
                            <label for="region3">Region three</label>
                        </div>
                        
                    </fieldset>
                </nav>
                
            </div>
        </div>      
        </div>

When clicking on the radio buttons, with the flyTo function, I recenter the map on the region concerned and display the markers of this region only.

    document.getElementById('region one').addEventListener('click', () => {
        map.flyTo({
            center: [5.041869163513184, 47.32464599609375],
            zoom: 6
        });
    })
    document.getElementById('region two').addEventListener('click', () => {
        map.flyTo({
            center: [4.6604809, 45.2968119],
            zoom: 6
        });
    })

Although the code is redundant, as I have about ten regions, it works. My goal is that when I click on the "all" button, the map returns to its initial position and displays all the markers, just like when the page loads.

As the click-on function, filterMarkers() allows you to filter the markers, I thought about adding on the input "all" : setAllMarkersVisible(), but it doesn't work. I also can't find a way to display all the markers via a function.

Is there a simple way, through the different functions present, or with additional code, to reset the map ? (restore all markers and fitBounds)

2
  • There is inconsistency between your question and the code above. You write that you switched to radio buttons, but Region three has checkbox. Also in function filterMarkers you are referring to check boxes input type, not radio button.
    – TomazicM
    Commented Oct 3 at 16:53
  • Sorry TomazicM and really thank you for taking the time to read the question and correct me for the inconsistencies. I can't seem to make progress on this point (and many others).. Do you have an idea of how to proceed?
    – blogob
    Commented Oct 4 at 2:56

1 Answer 1

1

One possible solution would go like this, requiring a few changes to the code.

Call to filterMarkers in definition of radio buttons requires parameter this to get DOM element from where it is called:

<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 one</label>
  </div>
  <div>
    <input id="region2" type="radio" name="radio" onclick="filterMarkers(this)"/>
    <label for="region2">Region two</label>
  </div>
  <div>
    <input id="region3" type="radio" name="radio" onclick="filterMarkers(this)"/>
    <label for="region3">Region three</label>
  </div>
</fieldset>

Bounding boxes of regions are calculated and stored in regions object, using turf.js library:

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));
    console.log(bbox);
    regions[checkbox.id] = bbox;
  }
});

Call map.fitBounds with bound from regions object is added to function filterMarkers function:

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}
  });
}

Call map.flyTo to initial map position is added to function setAllMarkersVisible:

function setAllMarkersVisible(mapSource) {
  mapSource.setData(stores);
  map.flyTo({
    center: [2.3514998, 48.85661], // starting position
    zoom: 5, // starting zoom
  });
}

Instead of initial map position it could also be call map.fitBounds to bounding box of all regions, calculated with turf.bbox(stores).

Here is working JSFiddle (sidebar is hidden): https://jsfiddle.net/TomazicM/pgn8bywr/

10
  • TomazicM You solved my problem in no time, once again, while I've been there for several days!! I don't know turf, so I don't understand everything and could never have done it alone. You solved the problem of the reset button, zooming on the region and in addition to clicking on the clusters (I don't know why I had to double click on it to zoom).
    – blogob
    Commented Oct 4 at 10:53
  • Really, thank you so much, I'm impressed and really appreciate your help :).. I still have two problems, the page jump when clicking on the sidebar links, and the color of the clusters. I continue my research:) thanks again !!
    – blogob
    Commented Oct 4 at 10:53
  • I noticed that in your version, a click on a cluster is enough to activate the zoom, but until then, I had to double click to advance in the zoom. Apart from your additional code, it doesn't seem to me that you have touched the rest. Do you have any idea what was preventing clusters from zooming in with a single click?
    – blogob
    Commented Oct 4 at 19:21
  • That's have Mapbox maps behave: double click on map means zoom in, click on cluster means zoom in enough so that individual markers are displayed.
    – TomazicM
    Commented Oct 4 at 19:32
  • in fact what I meant is that on your example, a single click on a cluster icon is enough for the map to zoom, like in the example of Mapbox docs.mapbox.com/mapbox-gl-js/example/cluster . In my local version, I had to double-click on the cluster icon to make it zoom. And apart from the code you added, I don't see what makes the difference.
    – blogob
    Commented Oct 4 at 23:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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