diff --git a/app/src/MaplibreMap.tsx b/app/src/MaplibreMap.tsx index a129786..1cb320a 100644 --- a/app/src/MaplibreMap.tsx +++ b/app/src/MaplibreMap.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { renderToString } from "react-dom/server"; import { PMTiles, TileType } from "../../js/index"; import { Protocol } from "../../js/adapters"; @@ -50,30 +50,104 @@ const Options = styled("div", { zIndex: 9999, }); +const CheckboxLabel = styled("label", { + display: "flex", + gap: 6, + cursor: "pointer", +}); + +const LayersVisibilityList = styled("ul", { + listStyleType: "none", +}); + const FeaturesProperties = (props: { features: MapGeoJSONFeature[] }) => { - const fs = props.features.map((f, i) => { - let tmp: [string, string][] = []; - for (var key in f.properties) { - tmp.push([key, f.properties[key]]); + return ( + + {props.features.map((f, i) => ( + + + {(f.layer as any)["source-layer"]} + ({f.geometry.type}) + + + {Object.entries(f.properties).map(([key, value], i) => ( + + + + + ))} +
{key}{value}
+
+ ))} +
+ ); +}; + +interface LayerVisibility { + id: string; + visible: boolean; +} + +const LayersVisibilityController = (props: { layers: LayerVisibility[], onChange: (layers: LayerVisibility[]) => void }) => { + const {layers, onChange} = props; + const allLayersCheckboxRef = useRef(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) => ( - - {d[0]} - {d[1]} - - )); - + if (!props.layers.length) { return ( - -
- {(f.layer as any)["source-layer"]} -
- {rows}
-
+ <> +

Layers

+ No vector layers found + ); - }); - return {fs}; + } + + const toggleAllLayers = () => { + const someLayersAreHidden = visibleLayersCount !== layers.length; + onChange(layers.map(l => ({...l, visible: someLayersAreHidden}))); + }; + + const toggleLayer = (event: React.ChangeEvent) => { + const layerId = event.target.getAttribute("data-layer-id"); + onChange(layers.map(l => (l.id === layerId ? {...l, visible: !l.visible} : l))); + }; + + return ( + <> +

Layers

+ + + All layers + + + {props.layers.map(({id, visible}) => ( +
  • + + + {id} + +
  • + ))} +
    + + ); }; const rasterStyle = async (file: PMTiles): Promise => { @@ -188,26 +262,29 @@ const vectorStyle = async (file: PMTiles): Promise => { const bounds = [header.minLon, header.minLat, header.maxLon, header.maxLat]; return { - version: 8, - sources: { - source: { - type: "vector", - tiles: ["pmtiles://" + file.source.getKey() + "/{z}/{x}/{y}"], - minzoom: header.minZoom, - maxzoom: header.maxZoom, - bounds: bounds, - }, - basemap: { - type: "vector", - tiles: [ - "https://api.protomaps.com/tiles/v2/{z}/{x}/{y}.pbf?key=1003762824b9687f", - ], - maxzoom: 14, - bounds: bounds, + style: { + version: 8, + sources: { + source: { + type: "vector", + tiles: ["pmtiles://" + file.source.getKey() + "/{z}/{x}/{y}"], + minzoom: header.minZoom, + maxzoom: header.maxZoom, + bounds: bounds, + }, + basemap: { + type: "vector", + tiles: [ + "https://api.protomaps.com/tiles/v2/{z}/{x}/{y}.pbf?key=1003762824b9687f", + ], + 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", - layers: layers, + layersVisibility: vector_layers.map((l: any) => ({id: l.id, visible: true})), }; }; @@ -216,6 +293,7 @@ function MaplibreMap(props: { file: PMTiles }) { let [hamburgerOpen, setHamburgerOpen] = useState(true); let [showAttributes, setShowAttributes] = useState(false); let [showTileBoundaries, setShowTileBoundaries] = useState(false); + let [layersVisibility, setLayersVisibility] = useState([]); const mapRef = useRef(null); const hoveredFeaturesRef = useRef>(new Set()); @@ -239,6 +317,15 @@ function MaplibreMap(props: { file: PMTiles }) { 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(() => { let protocol = new Protocol(); maplibregl.addProtocol("pmtiles", protocol.tile); @@ -327,8 +414,9 @@ function MaplibreMap(props: { file: PMTiles }) { let style = await rasterStyle(props.file); map.setStyle(style); } else { - let style = await vectorStyle(props.file); + let {style, layersVisibility} = await vectorStyle(props.file); map.setStyle(style); + setLayersVisibility(layersVisibility); } } }; @@ -344,21 +432,27 @@ function MaplibreMap(props: { file: PMTiles }) {

    Filter

    Popup

    - - + + + show attributes +

    Tiles

    - + + show tile boundaries + + -
    ) : null}