Digital Geography

31. July 2019

QGIS2web with interactive filters

It’s been a while, since I last developed things for qgis2web. To be honest: I lost focus on the whole project after we decided to merge qgis2leaf with the openlayers exporter back in 2015. Tom Chadwin and many others did a great job and managed to develop the state of-the-art plugin when it comes to publishing content right from within QGIS. But as I was lucky I had the chance to develop some interesting things for QGIS2web in the last weeks.

QGIS2web: the idea

In the past I already showed some ways of filtering a leaflet based webmap with simple Javascript and/or MaterializeCSS. This is a pretty much straightforward approach if you have static data: If you know the data model, it is easy to create the right filters and HTML elements to filter the map. Making this generic, is a bit more challenging:
  • Field-types are quite diverse in QGIS and need to be mapped to 4-5 main filter elements
  • The filters may or may not apply to all or just a single layer, depending on the attributes each layer has
  • The ranges, lists of each filter must be consolidated (remove duplicates, determine min/max values), treat of NULL
  • Keep in mind cross browser functionality for date/time/datetime inputs
When it comes to the selects in HTML I’ve used the following set of predefined options:
  • Text selection list for strings (multiple options)
  • Selection list for booleans
  • integer slider
  • double slider
  • combined date/time/datetime select
The map should be split in two parts: one for the map and one for the “filter menu”. Furthermore: The main code for qgis2web should be untouched as much as possible to make regression testing still possible.

QGIS2web filters: the approach for the selects

The main approach was:
  • get attributes of each layer
  • map the field types to a select type
  • determine the select entries
  • create HTML elements using JS after main code of QGIS2web for each selected attribute
  • add function for each filter type and apply filter selection of all filters to all layers
The ugly part in terms of coding is: all HTML elements are created with JavaScript and written to the file using Python:
if filterItems[item]["type"] in ["str", "bool"]:
	endHTML += """
	document.getElementById("menu").appendChild(
	document.createElement("div")); #placeholder DIV
	var div_{nameS} = document.createElement('div'); #This is the div which holds the select
	div_{nameS}.id = "div_{name}";
	div_{nameS}.className= "filterselect";
	document.getElementById("menu").appendChild(div_{nameS});
	sel_{nameS} = document.createElement('select'); #this is the select itself
	sel_{nameS}.multiple = true;
	sel_{nameS}.id = "sel_{name}";
	var {nameS}_options_str = "<option value='' unselected></option>";
	sel_{nameS}.onchange = function(){{filterFunc()}}; #runs the same functio for all filters in the map
""".format(name=itemName, nameS=safeName(itemName)) #safename removes unwanted strings in a attribute name
	for entry in filterItems[item]["values"]: #populating the select options
		endHTML += """
{nameS}_options_str  += '<option value="{e}">{e}</option>';
			""".format(e=entry, name=itemName,
					   nameS=safeName(itemName))
	endHTML += """
	sel_{nameS}.innerHTML = {nameS}_options_str;
	div_{nameS}.appendChild(sel_{nameS});
	var lab_{nameS} = document.createElement('div');
	lab_{nameS}.innerHTML = '{name}';
	lab_{nameS}.className = 'filterLabel';
	div_{nameS}.appendChild(lab_{nameS});
	""".format(name=itemName, nameS=safeName(itemName))
The same code would be much easier to read and to write using HTML. But hey:

QGIS2web: dealing with the selections

The function, which is applied to all layers, gets the underlying JSON data of a layer, determine, whether the attribut is part of the properties or not and then removes entries which not meet the selected criteria (JavaScript):
map.eachLayer(function(lyr){
	if ("options" in lyr && "dataVar" in lyr["options"]){
		features = this[lyr["options"]["dataVar"]].features.slice(0);
		try{
			for (key in Filters){
				if (Filters[key] == "str" || Filters[key] == "bool"){
					var selection = [];
					for (option in Array.from(document.getElementById("sel_" + key).selectedOptions)){
						selection.push(document.getElementById("sel_"+key).selectedOptions[option].value);
					}
					try{
					    if (key in features[0].properties){
							for (i = features.length - 1;i >= 0; --i){
								if (selection.indexOf(features[i].properties[key])<0 && selection.length>0) {
									features.splice(i,1);
								}
							}
					    }
					} catch(err){
					}
				}
			} catch(err){
		}
		this[lyr["options"]["layerName"]].clearLayers();
		this[lyr["options"]["layerName"]].addData(features);
		}
	})
}
The code for other selects is quite similar and follows the same principles. Yet the result is really good at first sight:

all selects possible for one webmap in qgis2web wxport

And you can explore a small example with area and name filters here (full size link):
The filter functionality is part of the latest experimental release 3.8.0 on github as well as in your plugin handler if you accept experimental plugins as well. If you see any issues in handling with data, please be so kind and drop a comment on github.