Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(layer): add option to limit layer list by extent BM-883 #3344

Merged
merged 8 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading