add vector layers visibility controller

This commit is contained in:
kajkal
2023-03-26 14:08:14 +02:00
parent e21d1b5c76
commit b3631def6b

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import { renderToString } from "react-dom/server"; import { renderToString } from "react-dom/server";
import { PMTiles, TileType } from "../../js/index"; import { PMTiles, TileType } from "../../js/index";
import { Protocol } from "../../js/adapters"; import { Protocol } from "../../js/adapters";
@@ -50,30 +50,104 @@ const Options = styled("div", {
zIndex: 9999, zIndex: 9999,
}); });
const CheckboxLabel = styled("label", {
display: "flex",
gap: 6,
cursor: "pointer",
});
const LayersVisibilityList = styled("ul", {
listStyleType: "none",
});
const FeaturesProperties = (props: { features: MapGeoJSONFeature[] }) => { const FeaturesProperties = (props: { features: MapGeoJSONFeature[] }) => {
const fs = props.features.map((f, i) => { return (
let tmp: [string, string][] = []; <PopupContainer>
for (var key in f.properties) { {props.features.map((f, i) => (
tmp.push([key, f.properties[key]]); <FeatureRow key={i}>
<span>
<strong>{(f.layer as any)["source-layer"]}</strong>
<span style={{fontSize: "0.8em"}}> ({f.geometry.type})</span>
</span>
<table>
{Object.entries(f.properties).map(([key, value], i) => (
<tr key={i}>
<td>{key}</td>
<td>{value}</td>
</tr>
))}
</table>
</FeatureRow>
))}
</PopupContainer>
);
};
interface LayerVisibility {
id: string;
visible: boolean;
}
const LayersVisibilityController = (props: { layers: LayerVisibility[], onChange: (layers: LayerVisibility[]) => void }) => {
const {layers, onChange} = props;
const allLayersCheckboxRef = useRef<HTMLInputElement>(null);
const visibleLayersCount = layers.filter(l => l.visible).length;
const indeterminate = visibleLayersCount > 0 && visibleLayersCount !== layers.length;
useEffect(() => {
if (allLayersCheckboxRef.current) {
allLayersCheckboxRef.current.indeterminate = indeterminate;
} }
}, [indeterminate]);
const rows = tmp.map((d, i) => ( if (!props.layers.length) {
<tr key={i}>
<td>{d[0]}</td>
<td>{d[1]}</td>
</tr>
));
return ( return (
<FeatureRow key={i}> <>
<div> <h4>Layers</h4>
<strong>{(f.layer as any)["source-layer"]}</strong> <span>No vector layers found</span>
</div> </>
<table>{rows}</table>
</FeatureRow>
); );
}); }
return <PopupContainer>{fs}</PopupContainer>;
const toggleAllLayers = () => {
const someLayersAreHidden = visibleLayersCount !== layers.length;
onChange(layers.map(l => ({...l, visible: someLayersAreHidden})));
};
const toggleLayer = (event: React.ChangeEvent<HTMLInputElement>) => {
const layerId = event.target.getAttribute("data-layer-id");
onChange(layers.map(l => (l.id === layerId ? {...l, visible: !l.visible} : l)));
};
return (
<>
<h4>Layers</h4>
<CheckboxLabel>
<input
ref={allLayersCheckboxRef}
type="checkbox"
checked={visibleLayersCount === layers.length}
onChange={toggleAllLayers}
/>
<em>All layers</em>
</CheckboxLabel>
<LayersVisibilityList>
{props.layers.map(({id, visible}) => (
<li key={id}>
<CheckboxLabel style={{paddingLeft: 8}}>
<input
type="checkbox"
checked={visible}
onChange={toggleLayer}
data-layer-id={id}
/>
{id}
</CheckboxLabel>
</li>
))}
</LayersVisibilityList>
</>
);
}; };
const rasterStyle = async (file: PMTiles): Promise<any> => { const rasterStyle = async (file: PMTiles): Promise<any> => {
@@ -188,26 +262,29 @@ const vectorStyle = async (file: PMTiles): Promise<any> => {
const bounds = [header.minLon, header.minLat, header.maxLon, header.maxLat]; const bounds = [header.minLon, header.minLat, header.maxLon, header.maxLat];
return { return {
version: 8, style: {
sources: { version: 8,
source: { sources: {
type: "vector", source: {
tiles: ["pmtiles://" + file.source.getKey() + "/{z}/{x}/{y}"], type: "vector",
minzoom: header.minZoom, tiles: ["pmtiles://" + file.source.getKey() + "/{z}/{x}/{y}"],
maxzoom: header.maxZoom, minzoom: header.minZoom,
bounds: bounds, maxzoom: header.maxZoom,
}, bounds: bounds,
basemap: { },
type: "vector", basemap: {
tiles: [ type: "vector",
"https://api.protomaps.com/tiles/v2/{z}/{x}/{y}.pbf?key=1003762824b9687f", tiles: [
], "https://api.protomaps.com/tiles/v2/{z}/{x}/{y}.pbf?key=1003762824b9687f",
maxzoom: 14, ],
bounds: bounds, maxzoom: 14,
bounds: bounds,
},
}, },
glyphs: "https://cdn.protomaps.com/fonts/pbf/{fontstack}/{range}.pbf",
layers: layers,
}, },
glyphs: "https://cdn.protomaps.com/fonts/pbf/{fontstack}/{range}.pbf", layersVisibility: vector_layers.map((l: any) => ({id: l.id, visible: true})),
layers: layers,
}; };
}; };
@@ -216,6 +293,7 @@ function MaplibreMap(props: { file: PMTiles }) {
let [hamburgerOpen, setHamburgerOpen] = useState<boolean>(true); let [hamburgerOpen, setHamburgerOpen] = useState<boolean>(true);
let [showAttributes, setShowAttributes] = useState<boolean>(false); let [showAttributes, setShowAttributes] = useState<boolean>(false);
let [showTileBoundaries, setShowTileBoundaries] = useState<boolean>(false); let [showTileBoundaries, setShowTileBoundaries] = useState<boolean>(false);
let [layersVisibility, setLayersVisibility] = useState<LayerVisibility[]>([]);
const mapRef = useRef<maplibregl.Map | null>(null); const mapRef = useRef<maplibregl.Map | null>(null);
const hoveredFeaturesRef = useRef<Set<MapGeoJSONFeature>>(new Set()); const hoveredFeaturesRef = useRef<Set<MapGeoJSONFeature>>(new Set());
@@ -239,6 +317,15 @@ function MaplibreMap(props: { file: PMTiles }) {
mapRef.current!.showTileBoundaries = !showTileBoundaries; mapRef.current!.showTileBoundaries = !showTileBoundaries;
}; };
const handleLayersVisibilityChange = (layersVisibility: LayerVisibility[]) => {
setLayersVisibility(layersVisibility);
for (const {id, visible} of layersVisibility) {
mapRef.current!.setLayoutProperty(`${id}_fill`, "visibility", visible ? "visible" : "none");
mapRef.current!.setLayoutProperty(`${id}_stroke`, "visibility", visible ? "visible" : "none");
mapRef.current!.setLayoutProperty(`${id}_point`, "visibility", visible ? "visible" : "none");
}
}
useEffect(() => { useEffect(() => {
let protocol = new Protocol(); let protocol = new Protocol();
maplibregl.addProtocol("pmtiles", protocol.tile); maplibregl.addProtocol("pmtiles", protocol.tile);
@@ -327,8 +414,9 @@ function MaplibreMap(props: { file: PMTiles }) {
let style = await rasterStyle(props.file); let style = await rasterStyle(props.file);
map.setStyle(style); map.setStyle(style);
} else { } else {
let style = await vectorStyle(props.file); let {style, layersVisibility} = await vectorStyle(props.file);
map.setStyle(style); map.setStyle(style);
setLayersVisibility(layersVisibility);
} }
} }
}; };
@@ -344,21 +432,27 @@ function MaplibreMap(props: { file: PMTiles }) {
<Options> <Options>
<h4>Filter</h4> <h4>Filter</h4>
<h4>Popup</h4> <h4>Popup</h4>
<input <CheckboxLabel>
type="checkbox" <input
id="showAttributes" type="checkbox"
checked={showAttributes} checked={showAttributes}
onChange={toggleShowAttributes} onChange={toggleShowAttributes}
/> />
<label htmlFor="showAttributes">show attributes</label> show attributes
</CheckboxLabel>
<h4>Tiles</h4> <h4>Tiles</h4>
<input <CheckboxLabel>
type="checkbox" <input
id="showTileBoundaries" type="checkbox"
checked={showTileBoundaries} checked={showTileBoundaries}
onChange={toggleShowTileBoundaries} onChange={toggleShowTileBoundaries}
/>
show tile boundaries
</CheckboxLabel>
<LayersVisibilityController
layers={layersVisibility}
onChange={handleLayersVisibilityChange}
/> />
<label htmlFor="showTileBoundaries">show tile boundaries</label>
</Options> </Options>
) : null} ) : null}
</MapContainer> </MapContainer>