mapbox / mapbox/mapbox-gl-js

Layers Control port from leaflet

Open
#5,489 4 comments 7 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

feature :green_apple:
Dominant language
TypeScript
Stars
12.4k
Forks
2.4k
PR merge metrics
No merged PRs in 30d

Description

## Motivation
**What problem are we trying to solve?**
Support for layer selection with the layer control that matches Leaflet layer control.

**What use cases are we trying to accommodate?**
Developers migrate from Leaflet to Mapbox GL and need to keep the UX of layer selection unchanged.
That is ability to switch between base layers and toogle on/off overlays.

## Design Alternatives
**How could we accommodate the use cases above?**

The control is initialized with a callback that takes the list of all layers and returns two lists:
- layers that should be treated as base layers
- layers or **layer groups** that should be treated as overlays.

User can group multiple related layers that are selectable together and filter away whatever he/she is not intending to expose which is probably a reasonable approach in this case.

**Is "do nothing" an option?**
Well, the developers will then need to mock the leaflet control with same look and feel which would probably consume them several hours if they are well-familiar with the framework, but still this time can be avoided.

## Example
Let's say user treats all raster layers as base layers and groups other layers based on regex _layer-id_ matches.

```typescript
map.addControl(new LayerSwitchControl(layers => {
const baseLayers = layers.filter(layer => layer.type == 'raster');
const layerGroups = [
{ predicate: /(water|ocean)/, name: 'water'},
{ predicate: /landuse/, name: 'landuse' },
{ predicate: /(road|rail|highway)/, name: 'roads'},
{ predicate: /boundar/, name: 'boundaries'},
{ predicate: /buildings/, name: 'buildings'}
].map(filter => layers
// don't use any of layers designated for highlighting
.filter(layer => !layer.id.endsWith('selected'))
.filter(layer => filter.predicate.exec(layer.id))
.reduce((layerGroup, layer) => ({
...layerGroup,
layers: [...layerGroup.layers, layer]
}), { name: filter.name, layers: [] } as LayerGroup))
return {
baseLayers: baseLayers,
overlays: layerGroups
}
}) as any)
```

### Mock-Up

**What will this design look like to end users?**

![screen shot 2017-10-20 at 4 14 42 pm](https://user-images.githubusercontent.com/14032724/31811977-10be4214-b5b4-11e7-9c9c-2933a4325902.png)

**onMouseEnter**
![screen shot 2017-10-20 at 4 14 51 pm](https://user-images.githubusercontent.com/14032724/31811964-05268dee-b5b4-11e7-989d-38349986f69a.png)

### Implementation
**How you would implement the design in Javascript?**
LayersControl that toogles the layer visibility as a result of radio buttons and checkbox changes.
Something like:

```typescript
export class LayerSwitchControl implements MapboxGL.IControl {
controlGroup: HTMLDivElement | null = null

constructor(private layerSelector: (layers: MapboxGL.Layer[]) => { baseLayers: MapboxGL.Layer[], overlays: MapboxGL.Layer[] | LayerGroup[], initialBaseLayer?: MapboxGL.Layer }){}

isIn: boolean = false

onAdd(map: MapboxGL.Map): HTMLElement {
const container = map.getContainer()
const controlGroup = document.createElement('div')
controlGroup.className = `mapboxgl-ctrl-group mapboxgl-ctrl`
this.controlGroup = controlGroup

this.setAsButton()
this.controlGroup.addEventListener('mouseenter', event => {
if(this.isIn){ return }
this.isIn = true
this.setAsList(map)
})

this.controlGroup.addEventListener('mouseleave', event => {
if(!this.isIn){ return }
this.isIn = false
this.setAsButton()
})

return controlGroup
}

/// trimmed...

private createRadioList(map: MapboxGL.Map, baseLayers: MapboxGL.Layer[], initial?: MapboxGL.Layer): HTMLDivElement | null {
if(baseLayers.length == 0){ return null }

const listContainer = document.createElement('div')
const formElement = document.createElement('form')
listContainer.appendChild(formElement)

const initialIndex = initial ? baseLayers.indexOf(initial) || 0 : 0
baseLayers.forEach((layer, index) => {
formElement.appendChild(this.createInputBlockElement(layer.id, 'radio', 'base', index == initialIndex, event => {
baseLayers.forEach(layer => map.setLayoutProperty(layer.id, 'visibility', 'none'));
map.setLayoutProperty(layer.id, 'visibility', 'visible')
}))
})

return listContainer
}
}
```

**What parts of the Mapbox GL ecosystem will need to change to accomodate this design?**
Extra ~200lines Control in mapbox gl js.

So I have implemented this simple port. If you guys think it is reasonable to include within the SDK, I will create a pull request.

P.S: Also related to https://github.com/mapbox/mapbox-gl-js/issues/4468

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the MapboxGL.IControl entry point and the proposed LayerSwitchControl.onAdd implementation; review how map.addControl and setLayoutProperty are used in the example. Done means a control matching Leaflet's layer-selection UX, including base-layer radio selection and overlay or layer-group toggles.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, typescript
Domain
frontend, web-dev
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.