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

Example of the final layers control

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.