From e21d1b5c76bac506d6123e39da9a4da70e057ad2 Mon Sep 17 00:00:00 2001 From: kajkal Date: Sun, 26 Mar 2023 12:43:04 +0200 Subject: [PATCH 1/4] improve UX of "show attributes" mode --- app/src/MaplibreMap.tsx | 46 +++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/app/src/MaplibreMap.tsx b/app/src/MaplibreMap.tsx index c2d74c8..a129786 100644 --- a/app/src/MaplibreMap.tsx +++ b/app/src/MaplibreMap.tsx @@ -128,7 +128,18 @@ const vectorStyle = async (file: PMTiles): Promise => { "source-layer": layer.id, paint: { "fill-color": schemeSet3[i % 12], - "fill-opacity": 0.2, + "fill-opacity": [ + "case", + ["boolean", ["feature-state", "hover"], false], + 0.35, + 0.2, + ], + "fill-outline-color": [ + "case", + ["boolean", ["feature-state", "hover"], false], + "hsl(0,100%,90%)", + "rgba(0,0,0,0)", + ], }, filter: ["==", ["geometry-type"], "Polygon"], }); @@ -139,7 +150,12 @@ const vectorStyle = async (file: PMTiles): Promise => { "source-layer": layer.id, paint: { "line-color": schemeSet3[i % 12], - "line-width": 0.5, + "line-width": [ + "case", + ["boolean", ["feature-state", "hover"], false], + 2, + 0.5, + ], }, filter: ["==", ["geometry-type"], "LineString"], }); @@ -150,6 +166,12 @@ const vectorStyle = async (file: PMTiles): Promise => { "source-layer": layer.id, paint: { "circle-color": schemeSet3[i % 12], + "circle-radius": [ + "case", + ["boolean", ["feature-state", "hover"], false], + 6, + 5, + ], }, filter: ["==", ["geometry-type"], "Point"], }); @@ -195,12 +217,13 @@ function MaplibreMap(props: { file: PMTiles }) { let [showAttributes, setShowAttributes] = useState(false); let [showTileBoundaries, setShowTileBoundaries] = useState(false); const mapRef = useRef(null); + const hoveredFeaturesRef = useRef>(new Set()); // make it accessible in hook const showAttributesRef = useRef(showAttributes); useEffect(() => { showAttributesRef.current = showAttributes; - }); + }, [showAttributes]); const toggleHamburger = () => { setHamburgerOpen(!hamburgerOpen); @@ -208,6 +231,7 @@ function MaplibreMap(props: { file: PMTiles }) { const toggleShowAttributes = () => { setShowAttributes(!showAttributes); + mapRef.current!.getCanvas().style.cursor = !showAttributes ? "crosshair" : ""; }; const toggleShowTileBoundaries = () => { @@ -242,18 +266,28 @@ function MaplibreMap(props: { file: PMTiles }) { mapRef.current = map; map.on("mousemove", (e) => { + const hoveredFeatures = hoveredFeaturesRef.current; + for (const feature of hoveredFeatures) { + map.setFeatureState(feature, {hover: false}); + hoveredFeatures.delete(feature); + } + if (!showAttributesRef.current) { popup.remove(); return; } - var bbox = e.point; - var features = map.queryRenderedFeatures(bbox); + const {x, y} = e.point; + const r = 2; // radius around the point + var features = map.queryRenderedFeatures([[x-r, y-r], [x+r,y+r]]); // ignore the basemap features = features.filter((feature) => feature.source === "source"); - map.getCanvas().style.cursor = features.length ? "pointer" : ""; + for (const feature of features) { + map.setFeatureState(feature, {hover: true}); + hoveredFeatures.add(feature); + } let content = renderToString(); if (!features.length) { From b3631def6b946ab261e6839475a416101e46f555 Mon Sep 17 00:00:00 2001 From: kajkal Date: Sun, 26 Mar 2023 14:08:14 +0200 Subject: [PATCH 2/4] add vector layers visibility controller --- app/src/MaplibreMap.tsx | 198 +++++++++++++++++++++++++++++----------- 1 file changed, 146 insertions(+), 52 deletions(-) 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} From 3282e6f3259529bcb5d31bbcb5c3d392d54c4fb9 Mon Sep 17 00:00:00 2001 From: kajkal Date: Sun, 26 Mar 2023 14:10:33 +0200 Subject: [PATCH 3/4] simplify opening a new viewer tab --- app/src/App.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/App.tsx b/app/src/App.tsx index 3e0f5c3..f72bd8f 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import React, { useState, useEffect } from "react"; import { styled, globalStyles } from "./stitches.config"; import { PMTiles } from "../../js/index"; import { GitHubLogoIcon } from "@radix-ui/react-icons"; @@ -14,9 +14,10 @@ const Header = styled("div", { padding: "0 $2 0 $2", }); -const Title = styled("span", { +const Title = styled("a", { fontWeight: 500, - cursor: "pointer", + color: "unset", + textDecoration: "none", }); const GithubA = styled("a", { @@ -88,7 +89,8 @@ function App() { } }, [file]); - let clear = () => { + let clear = (event: React.MouseEvent) => { + event.preventDefault(); setFile(undefined); }; @@ -99,7 +101,7 @@ function App() { return (
    - PMTiles Viewer + PMTiles Viewer {GIT_SHA} From baea873997715d1955bcf85c5d040a42e1fc7a6a Mon Sep 17 00:00:00 2001 From: kajkal Date: Sun, 26 Mar 2023 14:58:29 +0200 Subject: [PATCH 4/4] prettier formatting --- app/src/App.tsx | 4 ++- app/src/MaplibreMap.tsx | 66 +++++++++++++++++++++++++++-------------- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/app/src/App.tsx b/app/src/App.tsx index f72bd8f..dda1fad 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -101,7 +101,9 @@ function App() { return (
    - PMTiles Viewer + + PMTiles Viewer + {GIT_SHA} diff --git a/app/src/MaplibreMap.tsx b/app/src/MaplibreMap.tsx index 1cb320a..45a7aba 100644 --- a/app/src/MaplibreMap.tsx +++ b/app/src/MaplibreMap.tsx @@ -67,7 +67,7 @@ const FeaturesProperties = (props: { features: MapGeoJSONFeature[] }) => { {(f.layer as any)["source-layer"]} - ({f.geometry.type}) + ({f.geometry.type}) {Object.entries(f.properties).map(([key, value], i) => ( @@ -88,11 +88,15 @@ interface LayerVisibility { visible: boolean; } -const LayersVisibilityController = (props: { layers: LayerVisibility[], onChange: (layers: LayerVisibility[]) => void }) => { - const {layers, onChange} = props; +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; + const visibleLayersCount = layers.filter((l) => l.visible).length; + const indeterminate = + visibleLayersCount > 0 && visibleLayersCount !== layers.length; useEffect(() => { if (allLayersCheckboxRef.current) { @@ -110,13 +114,17 @@ const LayersVisibilityController = (props: { layers: LayerVisibility[], onChange } const toggleAllLayers = () => { - const someLayersAreHidden = visibleLayersCount !== layers.length; - onChange(layers.map(l => ({...l, visible: someLayersAreHidden}))); + const visible = visibleLayersCount !== layers.length; + const newLayersVisibility = layers.map((l) => ({ ...l, visible })); + onChange(newLayersVisibility); }; 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))); + const newLayersVisibility = layers.map((l) => + l.id === layerId ? { ...l, visible: !l.visible } : l + ); + onChange(newLayersVisibility); }; return ( @@ -132,9 +140,9 @@ const LayersVisibilityController = (props: { layers: LayerVisibility[], onChange All layers - {props.layers.map(({id, visible}) => ( + {props.layers.map(({ id, visible }) => (
  • - + => { glyphs: "https://cdn.protomaps.com/fonts/pbf/{fontstack}/{range}.pbf", layers: layers, }, - layersVisibility: vector_layers.map((l: any) => ({id: l.id, visible: true})), + layersVisibility: vector_layers.map((l: any) => ({ + id: l.id, + visible: true, + })), }; }; @@ -309,7 +320,9 @@ function MaplibreMap(props: { file: PMTiles }) { const toggleShowAttributes = () => { setShowAttributes(!showAttributes); - mapRef.current!.getCanvas().style.cursor = !showAttributes ? "crosshair" : ""; + mapRef.current!.getCanvas().style.cursor = !showAttributes + ? "crosshair" + : ""; }; const toggleShowTileBoundaries = () => { @@ -317,14 +330,18 @@ function MaplibreMap(props: { file: PMTiles }) { mapRef.current!.showTileBoundaries = !showTileBoundaries; }; - const handleLayersVisibilityChange = (layersVisibility: LayerVisibility[]) => { + 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"); + const map = mapRef.current!; + for (const { id, visible } of layersVisibility) { + const visibility = visible ? "visible" : "none"; + map.setLayoutProperty(`${id}_fill`, "visibility", visibility); + map.setLayoutProperty(`${id}_stroke`, "visibility", visibility); + map.setLayoutProperty(`${id}_point`, "visibility", visibility); } - } + }; useEffect(() => { let protocol = new Protocol(); @@ -355,7 +372,7 @@ function MaplibreMap(props: { file: PMTiles }) { map.on("mousemove", (e) => { const hoveredFeatures = hoveredFeaturesRef.current; for (const feature of hoveredFeatures) { - map.setFeatureState(feature, {hover: false}); + map.setFeatureState(feature, { hover: false }); hoveredFeatures.delete(feature); } @@ -364,15 +381,18 @@ function MaplibreMap(props: { file: PMTiles }) { return; } - const {x, y} = e.point; + const { x, y } = e.point; const r = 2; // radius around the point - var features = map.queryRenderedFeatures([[x-r, y-r], [x+r,y+r]]); + var features = map.queryRenderedFeatures([ + [x - r, y - r], + [x + r, y + r], + ]); // ignore the basemap features = features.filter((feature) => feature.source === "source"); for (const feature of features) { - map.setFeatureState(feature, {hover: true}); + map.setFeatureState(feature, { hover: true }); hoveredFeatures.add(feature); } @@ -414,7 +434,7 @@ function MaplibreMap(props: { file: PMTiles }) { let style = await rasterStyle(props.file); map.setStyle(style); } else { - let {style, layersVisibility} = await vectorStyle(props.file); + let { style, layersVisibility } = await vectorStyle(props.file); map.setStyle(style); setLayersVisibility(layersVisibility); }