Leaflet – Filter Multiple Features with L.control.layers and Checkboxes

layer-controlleaflet

Working with leaflet.js. I'm trying to incorporate multiple filters in L.control.layers. I have a working layer control for changing basemaps and layers of markers with different years, but I would like to add an extra filter, namely wether or not the marker is active. Ideally, it would look like this:

Basemaps:
[radiobutton] Map 1
[radiobutton] Map 2

Year:
[checkbox] 2018
[checkbox] 2019

Status:
[checkbox] Active
[checkbox] Inactive

This means that it is not as simple as projecting certain layers on the map, but every time the values of the checkboxes change, all the properties must be checked and only those who fit the filters are projected. So, when for example Active and Inactive are checked, but 2018 and 2019 are unchecked, no markers would be projected on the map. If only Active and 2018 are checked, the map shows only those markers which are active and have the year 2018.

You can find my code below, or a working script here https://jsfiddle.net/m7wvnbjq/1/. I basically create separate layers which I can control with L.control.layers, but I'm looking for a way of joining all the features in one layer and having this layer be filtered according to the checkboxes.

<!DOCTYPE html>
<!-- import leaflet.css -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin="" />

<!-- import leaflet.js -->
<script src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js" integrity="sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew==" crossorigin=""></script>

<div id="map"></div>
#map {
  height: 500px;
}
// initialize basemaps
var OpenStreetMap_Mapnik = L.tileLayer(
  "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
    maxZoom: 19,
    attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
  }
);

var NL_basemap = L.tileLayer(
  "https://geodata.nationaalgeoregister.nl/tiles/service/wmts/brtachtergrondkaart/EPSG:3857/{z}/{x}/{y}.png", {
    minZoom: 6,
    maxZoom: 19,
    bounds: [
      [50.5, 3.25],
      [54, 7.6]
    ],
    attribution: 'Kaartgegevens &copy; <a href="kadaster.nl">Kadaster</a>'
  }
);

// collect basemaps in dict
var basemaps = {
  Map1: OpenStreetMap_Mapnik,
  Map2: NL_basemap
};

// initialize map
var mymap = L.map("map", {
  layers: OpenStreetMap_Mapnik
}).setView([52.35274, 4.894784], 10);

// create data in two separate layers
var geoJSON_data_2018 = [{
    type: "Feature",
    properties: {
      title: "Marker1",
      year: 2018,
      activity: "Inactive"
    },
    geometry: {
      type: "Point",
      coordinates: [4.7, 52.3]
    }
  },
  {
    type: "Feature",
    properties: {
      title: "Marker2",
      year: 2018,
      activity: "Active"
    },
    geometry: {
      type: "Point",
      coordinates: [5, 52.4]
    }
  },
  {
    type: "Feature",
    properties: {
      title: "Marker3",
      year: 2018,
      activity: "Inactive"
    },
    geometry: {
      type: "Point",
      coordinates: [4.8, 52.3]
    }
  },
  {
    type: "Feature",
    properties: {
      title: "Marker4",
      year: 2018,
      activity: "Active"
    },
    geometry: {
      type: "Point",
      coordinates: [4.9, 52.2]
    }
  },
  {
    type: "Feature",
    properties: {
      title: "Marker5",
      year: 2018,
      activity: "Inactive"
    },
    geometry: {
      type: "Point",
      coordinates: [5, 52.25]
    }
  }
];

var geoJSON_data_2019 = [{
    type: "Feature",
    properties: {
      title: "Marker6",
      year: 2019,
      activity: "Inactive"
    },
    geometry: {
      type: "Point",
      coordinates: [4.8, 52.4]
    }
  },
  {
    type: "Feature",
    properties: {
      title: "Marker7",
      year: 2019,
      activity: "Active"
    },
    geometry: {
      type: "Point",
      coordinates: [5.1, 52.5]
    }
  },
  {
    type: "Feature",
    properties: {
      title: "Marker8",
      year: 2019,
      activity: "Inactive"
    },
    geometry: {
      type: "Point",
      coordinates: [4.9, 52.4]
    }
  },
  {
    type: "Feature",
    properties: {
      title: "Marker9",
      year: 2019,
      activity: "Active"
    },
    geometry: {
      type: "Point",
      coordinates: [5, 52.3]
    }
  },
  {
    type: "Feature",
    properties: {
      title: "Marker10",
      year: 2019,
      activity: "Inactive"
    },
    geometry: {
      type: "Point",
      coordinates: [5.1, 52.35]
    }
  }
];

// collect the two layers in dict
var yearLayers = {
  2018: L.geoJson(geoJSON_data_2018),
  2019: L.geoJson(geoJSON_data_2019)
};

// add filter control
L.control.layers(basemaps, yearLayers, {
  collapsed: false
}).addTo(mymap);

In this post (Multiple on-the-fly filtering based on markers' features on leaflet) a solution is offered for filtering on multiple features, but here the checkboxes are outside the map.

My question is if it would be possible to have it incorporated in L.control.layers, so that it is nicely concealed in the map and I don't have to manually add all the available checkboxes (in my real script I have a for-loop who adds checkboxes for all the values in my data).

Best Answer

To dynamically change GeoJSON layer filter with L.control.layers control, dummy nodisplayed layers can be used. These layers are used:

  • to have desired entries in L.control.layers control;
  • to filter GeoJSON layer features depending on dummy layer beeing included in the map (layer checked in the control) or not;
  • to trigger map's overlayadd and overlayremove events and reload GeoJSON layer with current filter.

If your two data sources are joined into single geoJSON_data source, your code could then look something like this:

var selectLayer = [];    
for (var i = 0; i < 4; i++) {
  selectLayer[i] = L.polyline([[0, 0], [0, 0]], {stroke: false, interactive: false});
}

var selectLayers = {
  '2018': selectLayer[0],
  '2019': selectLayer[1],
  'Active': selectLayer[2],
  'Inactive': selectLayer[3]
};

var geojsonLayer = L.geoJson(geoJSON_data, {
  filter: function (feature) {
    var cond1 = mymap.hasLayer(selectLayers[feature.properties.year]);
    var cond2 = mymap.hasLayer(selectLayers[feature.properties.activity]);
          
    return (cond1 && cond2);
  }
}).addTo(mymap);

L.control.layers(basemaps, selectLayers, {
  collapsed: false
}).addTo(mymap);

mymap.on('overlayadd overlayremove', function(evt) {
  geojsonLayer.clearLayers()
  geojsonLayer.addData(geoJSON_data)
});
Related Question