Skip to content

Commit

Permalink
feat(layer): add option to limit layer list by extent BM-883 (#3344)
Browse files Browse the repository at this point in the history
### Motivation

When browsing around the map the layers list is very long and its hard
to know could be shown from the current view

### Modifications

Add a checkbox to limit the layer list by the approximate bounding box
of the map.

### Verification


![image](https://github.com/user-attachments/assets/cd8936dd-be34-436d-a11a-1f995c0fbba0)
  • Loading branch information
blacha authored Sep 20, 2024
1 parent 77d3584 commit 4bc33ff
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 10 deletions.
99 changes: 89 additions & 10 deletions packages/landing/src/components/layer.switcher.dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Bounds, GoogleTms, Projection } from '@basemaps/geo';
import { ChangeEventHandler, Component, ReactNode } from 'react';
import Select from 'react-select';

Expand Down Expand Up @@ -29,7 +30,10 @@ export interface Option {

export interface LayerSwitcherDropdownState {
layers?: Map<string, LayerInfo>;
/** Should the map be zoomed to the extent of the layer when the layer is changed */
zoomToExtent: boolean;
/** Should the drop down be limited to the approximate extent of the map */
filterToExtent: boolean;
currentLayer: string;
}

Expand All @@ -40,7 +44,7 @@ export class LayerSwitcherDropdown extends Component<unknown, LayerSwitcherDropd

constructor(p: unknown) {
super(p);
this.state = { zoomToExtent: true, currentLayer: 'unknown' };
this.state = { zoomToExtent: true, currentLayer: 'unknown', filterToExtent: false };
}

override componentDidMount(): void {
Expand Down Expand Up @@ -115,43 +119,94 @@ export class LayerSwitcherDropdown extends Component<unknown, LayerSwitcherDropd
window.history.pushState(null, '', `?${MapConfig.toUrl(Config.map)}`);
};

onZoomExtentChange: ChangeEventHandler<unknown> = (e) => {
const target = e.target as HTMLInputElement;
this.setState({ zoomToExtent: target.checked });
onZoomExtentChange: ChangeEventHandler<HTMLInputElement> = (e) => {
gaEvent(GaEvent.Ui, 'layer-list:zoomToExtent:' + e.target.checked);
this.setState({ zoomToExtent: e.target.checked });
};

onFilterExtentChange: ChangeEventHandler<HTMLInputElement> = (e) => {
gaEvent(GaEvent.Ui, 'layer-list:filterToExtent:' + e.target.checked);
this.setState({ filterToExtent: e.target.checked });
};

renderTotal(total: number, hidden: number): ReactNode | null {
if (total === 0) return null;
if (hidden > 0) {
return (
<p title={`${hidden} layers hidden by filter`}>
{total - hidden} / {total}
</p>
);
}
return <p title={`${total} layers`}>{total}</p>;
}

override render(): ReactNode {
const ret = this.makeOptions();

return (
<div className="LuiDeprecatedForms">
<h6>Layers</h6>
<h6 className="layers-title">Layers {this.renderTotal(ret.total, ret.hidden)}</h6>
<Select
options={ret.options}
onChange={this.onLayerChange}
value={ret.current}
classNamePrefix="layer-selector"
id="layer-selector"
/>
<div className="lui-input-group-wrapper">
<div
className="lui-input-group-wrapper"
style={{ display: 'flex', justifyContent: 'space-around', height: 48 }}
>
<div className="lui-checkbox-container">
<input type="checkbox" onChange={this.onFilterExtentChange} checked={this.state.filterToExtent} />
<label title="Filter the layer list to approximately the current map extent">
Filter by map view
{ret.hidden > 0 ? (
<p>
<b>{ret.hidden}</b> layers hidden
</p>
) : null}
</label>
</div>
<div className="lui-checkbox-container">
<input type="checkbox" onChange={this.onZoomExtentChange} checked={this.state.zoomToExtent} />
<label>Zoom to Extent</label>
<label title="On layer change zoom to the extent of the layer">Zoom to layer</label>
</div>
</div>
</div>
);
}

makeOptions(): { options: GroupedOptions[]; current: Option | null } {
if (this.state.layers == null || this.state.layers.size === 0) return { options: [], current: null };
makeOptions(): { options: GroupedOptions[]; current: Option | null; hidden: number; total: number } {
let hidden = 0;
let total = 0;
if (this.state.layers == null || this.state.layers.size === 0) return { options: [], current: null, hidden, total };
const categories: CategoryMap = new Map();
const currentLayer = this.state.currentLayer;
const filterToExtent = this.state.filterToExtent;

const location = Config.map.location;
const loc3857 = Projection.get(GoogleTms).fromWgs84([location.lon, location.lat]);
const tileSize = GoogleTms.tileSize * GoogleTms.pixelScale(Math.floor(location.zoom)); // width of 1 tile
// Assume the current bounds are 3x3 tiles, todo would be more correct to use the map's bounding box but we dont have access to it here
const bounds = new Bounds(loc3857[0], loc3857[1], 1, 1).scaleFromCenter(3 * tileSize, 3 * tileSize);

let current: Option | null = null;

for (const layer of this.state.layers.values()) {
if (ignoredLayers.has(layer.id)) continue;
if (!layer.projections.has(Config.map.tileMatrix.projection.code)) continue;
total++;
// Always show the current layer
if (layer.id !== currentLayer) {
// Limit all other layers to the extent if requested
if (filterToExtent && !doesLayerIntersect(bounds, layer)) {
hidden++;
continue;
}
}

const layerId = layer.category ?? 'Unknown';
const layerCategory = categories.get(layerId) ?? { label: layerId, options: [] };
const opt = { value: layer.id, label: layer.name.replace(` ${layer.category}`, '') };
Expand All @@ -168,6 +223,30 @@ export class LayerSwitcherDropdown extends Component<unknown, LayerSwitcherDropd
return 1;
}),
);
return { options: [...orderedCategories.values()], current: current };
return { options: [...orderedCategories.values()], current, hidden, total };
}
}

/**
* Determine if the bounds in EPSG:3857 intersects the provided layer
*
* TODO: It would be good to then use a more comprehensive intersection if the bounding box intersects,
* there are complex polygons inside the attribution layer that could be used but they do not have all
* the polygons
*
* @param bounds Bounding box in EPSG:3857
* @param layer layer to check
* @returns true if it intersects, false otherwise
*/
function doesLayerIntersect(bounds: Bounds, layer: LayerInfo): boolean {
// No layer information assume it intersects
if (layer.lowerRight == null || layer.upperLeft == null) return true;

// It is somewhat easier to find intersections in EPSG:3857
const ul3857 = Projection.get(GoogleTms).fromWgs84(layer.upperLeft);
const lr3857 = Projection.get(GoogleTms).fromWgs84(layer.lowerRight);

const layerBounds = Bounds.fromBbox([ul3857[0], ul3857[1], lr3857[0], lr3857[1]]);

return bounds.intersects(layerBounds);
}
14 changes: 14 additions & 0 deletions packages/landing/static/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ button {
margin-top: 8px;
}

.layers-title {
display: flex;
justify-content: space-between;
}

.layers-title p {
margin: 0;
}

.lui-menu-drawer .lui-checkbox-container p {
margin-top: 2px;
}

.lui-menu-drawer .about-links {
list-style: none;
padding: 0;
Expand Down Expand Up @@ -74,6 +87,7 @@ button {
.lui-menu-drawer h6 {
border-bottom: 1px solid #eaeaea;
padding-bottom: 4px;
margin-top: 24px;
margin-bottom: 8px;
}

Expand Down

0 comments on commit 4bc33ff

Please sign in to comment.