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:
(Fix Broken Image!)
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:
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.
The last post about our famous qgis2leaf plugin showed a glimpse of what it is capable of: exporting all your features from a QGIS project to a working…
When Mike Bostock published his (re-) implementation and demonstration of Visvalingams algorithm for line simplification he attracted considerable attention on that. What is a little bit strange in…
It’s really helpful to generate interactive filters right out of your plugin. Well done!
Very nice job ty!