A Custom Maplibre Control to Toggle Layer Visibility
In the previous post, I set up Maplibre (GL JS) with a Protomaps file. I also wanted to be able to toggle layers on and off, akin to the layers control in Leaflet. I did not immediately see an existing control in Maplibre that did what I had in mind, so I quickly threw together one myself. While what I made is not nearly as robust and complete as Leaflet’s, it is enough to scratch my itch.
The Idea
Essentially clicking the checkbox should remove or hide a set of predefined
layers from the Maplibre map. So if I click the “gc_run” checkbox and it had
been preconfigured to the layer IDs “runs” and “walks”, then the layers with
those IDs should (dis)appear. In practice I end up toggling their visibility
layout property between visible
and none
.
The Code
To make this, I followed an example custom control from the Maplibre docs and tweaked it to my needs. I threw in a bunch of comments to ensure you can follow along if you so desire.
class LayersControl {
constructor(ctrls) {
// This div will hold all the checkboxes and their labels
this._container = document.createElement("div");
this._container.classList.add(
// Built-in classes for consistency
"maplibregl-ctrl",
"maplibregl-ctrl-group",
// Custom class, see later
"layers-control",
);
// Might be cleaner to deep copy these instead
this._ctrls = ctrls;
// Direct access to the input elements so I can decide which should be
// checked when adding the control to the map.
this._inputs = [];
// Create the checkboxes and add them to the container
for (const key of Object.keys(this._ctrls)) {
let labeled_checkbox = this._createLabeledCheckbox(key);
this._container.appendChild(labeled_checkbox);
}
}
// Creates one checkbox and its label
_createLabeledCheckbox(key) {
let label = document.createElement("label");
label.classList.add("layer-control");
let text = document.createTextNode(key);
let input = document.createElement("input");
this._inputs.push(input);
input.type = "checkbox";
input.id = key;
// `=>` function syntax keeps `this` to the LayersControl object
// When changed, toggle all the layers associated with the checkbox via
// `this._ctrls`.
input.addEventListener("change", () => {
let visibility = input.checked ? "visible" : "none";
for (const layer of this._ctrls[input.id]) {
map.setLayoutProperty(layer, "visibility", visibility);
}
});
label.appendChild(input);
label.appendChild(text);
return label;
}
onAdd(map) {
this._map = map;
// For every checkbox, find out if all its associated layers are visible.
// Check the box if so.
for (const input of this._inputs) {
// List of all layer ids associated with this checkbox
let layers = this._ctrls[input.id];
// Check whether every layer is currently visible
let is_visible = true;
for (const layername of layers) {
is_visible =
is_visible &&
this._map.getLayoutProperty(layername, "visibility") !== "none";
}
input.checked = is_visible;
}
return this._container;
}
onRemove(map) {
// Not sure why we have to do this ourselves since we are not the ones
// adding us to the map.
// Copied from their example so keeping it in.
this._container.parentNode.removeChild(this._container);
// This might be to help garbage collection? Also from their example.
// Or perhaps to ensure calls to this object do not change the map still
// after removal.
this._map = undefined;
}
}
Also some minor CSS additions for our classes.
.layers-control .layer-control {
display: block;
padding: 0.2em;
}
The Usage
Creating a layers control is done as follows.
// Set up the dictionary
let label_to_layer_ids = {
firstlabelcheckbox: ["layerid1"],
labelcheckboxwithmultiplelayers: ["layerid2", "layerid3", "layerid4"],
};
// Create control
let lc = new LayersControl(label_to_layer_ids);
Then adding it to the map with its addControl
. I ensure it happens after the
load
event for the map since I have had trouble adding controls before that
moment. You could add that creation to the event too of course.
map.on("load", function() {
map.addControl(lc);
});
And that should get you going!
The Complaint
Why not make a nice repository / npm library / … you can work with? Because I am quite sure I cannot be bothered maintaining this or handling bug reports. Maybe that will change in the future.