From 9962b4e344d46715f86759801cfacbc1886e5d8e Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Mon, 13 Jun 2022 16:48:58 +0800 Subject: [PATCH] improvements to inspector app --- app/src/App.tsx | 27 ++++++- app/src/Inspector.tsx | 33 +++++++-- app/src/LeafletMap.tsx | 52 +++++++++++-- app/src/Loader.tsx | 148 ++++++++++++++++--------------------- app/src/MaplibreMap.tsx | 110 +++++++++++++++++++-------- app/src/Start.tsx | 2 +- app/src/stitches.config.ts | 5 +- js/index.ts | 2 +- 8 files changed, 241 insertions(+), 138 deletions(-) diff --git a/app/src/App.tsx b/app/src/App.tsx index 477c5cd..a2646f5 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -1,12 +1,31 @@ import { useState, useEffect } from "react"; import { styled, globalStyles } from "./stitches.config"; import { PMTiles } from "../../js"; +import { GitHubLogoIcon } from "@radix-ui/react-icons"; import Start from "./Start"; import Loader from "./Loader"; const Header = styled("div", { height: "$4", + display: "flex", + alignItems: "center", + padding: "0 $2 0 $2", +}); + +const Title = styled("span", { + fontWeight: 500, + cursor: "pointer", +}); + +const GithubA = styled("a", { + color: "white", + textDecoration: "none", + fontSize: "$1", +}); + +const GithubLink = styled("span", { + marginLeft: "auto", }); const GIT_SHA = (import.meta.env.VITE_GIT_SHA || "").substr(0, 8); @@ -37,8 +56,12 @@ function App() { return (
- pmtiles viewer | github | toggle |{" "} - {GIT_SHA} + PMTiles Viewer + + + {GIT_SHA} + +
{file ? : }
diff --git a/app/src/Inspector.tsx b/app/src/Inspector.tsx index eb7bd6f..75bc289 100644 --- a/app/src/Inspector.tsx +++ b/app/src/Inspector.tsx @@ -8,16 +8,24 @@ import { VectorTile, VectorTileFeature } from "@mapbox/vector-tile"; import { path } from "d3-path"; import { schemeSet3 } from "d3-scale-chromatic"; import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { introspectTileType, TileType } from "./Loader"; const TableContainer = styled("div", { - height: "calc(100vh - $4)", + height: "calc(100vh - $4 - $4)", overflowY: "scroll", width: "50%", + padding: "$2", }); const Pane = styled("div", { width: "50%", backgroundColor: "black", + display: "flex", + alignItems: "center", + justifyContent: "center", + backgroundSize: "20px 20px", + backgroundImage: + "linear-gradient(to right, #222 1px, transparent 1px),linear-gradient(to bottom, #222 1px, transparent 1px);", }); const TableRow = styled( @@ -58,6 +66,7 @@ interface Layer { } interface Feature { + layerName: string; path: string; type: number; id: number; @@ -148,7 +157,14 @@ const FeatureProperties = (props: { feature: Feature }) => { )); - return {rows}; + return ( + + {props.feature.layerName} + + {rows} +
+
+ ); }; const VectorPreview = (props: { file: PMTiles; entry: Entry }) => { @@ -193,6 +209,7 @@ const VectorPreview = (props: { file: PMTiles; entry: Entry }) => { type: feature.type, id: feature.id, properties: feature.properties, + layerName: name, }); } newLayers.push({ features: features, name: name }); @@ -248,11 +265,14 @@ const RasterPreview = (props: { file: PMTiles; entry: Entry }) => { function Inspector(props: { file: PMTiles }) { let [entryRows, setEntryRows] = useState([]); let [selectedEntry, setSelectedEntry] = useState(null); + let [tileType, setTileType] = useState(TileType.UNKNOWN); useEffect(() => { let fn = async () => { let entries = await props.file.root_entries(); + let tileType = await introspectTileType(props.file); setEntryRows(entries); + setTileType(tileType); }; fn(); @@ -263,9 +283,12 @@ function Inspector(props: { file: PMTiles }) { )); let tilePreview =
; - if (selectedEntry) { - tilePreview = ; - } else { + if (selectedEntry && tileType) { + if (tileType === TileType.MVT) { + tilePreview = ; + } else { + tilePreview = ; + } } return ( diff --git a/app/src/LeafletMap.tsx b/app/src/LeafletMap.tsx index 2c04056..bbc3aac 100644 --- a/app/src/LeafletMap.tsx +++ b/app/src/LeafletMap.tsx @@ -1,26 +1,62 @@ import { useState, useEffect } from "react"; -import { PMTiles, leafletLayer } from "../../js"; +import { PMTiles, leafletLayer as rasterLeafletLayer } from "../../js"; +import { leafletLayer as vectorLeafletLayer } from "protomaps"; import { styled } from "./stitches.config"; +import { introspectTileType, TileType } from "./Loader"; import L from "leaflet"; import "leaflet/dist/leaflet.css"; const MapContainer = styled("div", { - height: "calc(100vh - $4)", + height: "calc(100vh - $4 - $4)", }); -function LeafletMap(props: { file: PMTiles; tileType: string | null }) { +function LeafletMap(props: { file: PMTiles }) { + var map: L.Map; + var currentLayer: L.Layer; useEffect(() => { - const map = L.map("map").setView([0, 0], 0); - leafletLayer(props.file, { - attribution: - '© OpenStreetMap contributors', - }).addTo(map); + map = L.map("map").setView([0, 0], 0); return () => { map.remove(); }; }, []); + useEffect(() => { + if (currentLayer) currentLayer.remove(); + let initStyle = async () => { + if (map) { + let tileType = await introspectTileType(props.file); + if (tileType === TileType.PNG || tileType == TileType.JPG) { + currentLayer = rasterLeafletLayer(props.file, { + attribution: + '© OpenStreetMap contributors', + }); + currentLayer.addTo(map); + } else { + console.error("leaflet vector preview not yet implemented"); + // let metadata = await props.file.metadata(); + // let rules: PaintRule[] = []; + + // if (metadata.json) { + // let root = JSON.parse(metadata.json); + // if (root.tilestats) { + // for (let layer of root.tilestats.layers) { + // if (layer.geometry === "Polygon") { + // } else if (layer.geometry === "LineString") { + // } else { + // } + // } + // } + // } + + // currentLayer = vectorLeafletLayer(props.file, {paintRules:rules,labelRules:[]}) + } + } + }; + + initStyle(); + }, []); + return ; } diff --git a/app/src/Loader.tsx b/app/src/Loader.tsx index 64b51da..462a441 100644 --- a/app/src/Loader.tsx +++ b/app/src/Loader.tsx @@ -10,13 +10,20 @@ import { MagnifyingGlassIcon, ImageIcon } from "@radix-ui/react-icons"; import * as ToolbarPrimitive from "@radix-ui/react-toolbar"; import * as DialogPrimitive from "@radix-ui/react-dialog"; +export enum TileType { + UNKNOWN = 1, + PNG, + JPG, + MVT, + MVT_GZ, +} + const StyledToolbar = styled(ToolbarPrimitive.Root, { display: "flex", - padding: 10, + height: "$4", width: "100%", boxSizing: "border-box", minWidth: "max-content", - borderRadius: 6, backgroundColor: "white", boxShadow: `0 2px 10px "black"`, }); @@ -24,31 +31,15 @@ const StyledToolbar = styled(ToolbarPrimitive.Root, { const itemStyles = { all: "unset", flex: "0 0 auto", - color: "black", - height: 25, - padding: "0 5px", - borderRadius: 4, + color: "$black", display: "inline-flex", - fontSize: 13, - lineHeight: 1, + padding: "0 $1 0 $1", + fontSize: "$2", alignItems: "center", - justifyContent: "center", - "&:hover": { backgroundColor: "$hover", color: "blue" }, + "&:hover": { backgroundColor: "$hover", color: "$white" }, "&:focus": { position: "relative", boxShadow: `0 0 0 2px blue` }, }; -const StyledButton = styled( - ToolbarPrimitive.Button, - { - ...itemStyles, - paddingLeft: 10, - paddingRight: 10, - color: "white", - backgroundColor: "blue", - }, - { "&:hover": { color: "white", backgroundColor: "red" } } -); - const StyledLink = styled( ToolbarPrimitive.Link, { @@ -62,12 +53,6 @@ const StyledLink = styled( { "&:hover": { backgroundColor: "transparent", cursor: "pointer" } } ); -const StyledSeparator = styled(ToolbarPrimitive.Separator, { - width: 1, - backgroundColor: "black", - margin: "0 10px", -}); - const StyledToggleGroup = styled(ToolbarPrimitive.ToggleGroup, { display: "inline-flex", borderRadius: 4, @@ -79,7 +64,7 @@ const StyledToggleItem = styled(ToolbarPrimitive.ToggleItem, { backgroundColor: "white", marginLeft: 2, "&:first-child": { marginLeft: 0 }, - "&[data-state=on]": { backgroundColor: "red", color: "blue" }, + "&[data-state=on]": { backgroundColor: "$primary", color: "white" }, }); const StyledOverlay = styled(DialogPrimitive.Overlay, { @@ -91,40 +76,52 @@ const StyledOverlay = styled(DialogPrimitive.Overlay, { }); const StyledContent = styled(DialogPrimitive.Content, { - backgroundColor: "white", + backgroundColor: "$black", borderRadius: 6, - boxShadow: - "hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px", position: "fixed", top: "50%", left: "50%", transform: "translate(-50%, -50%)", width: "90vw", - maxWidth: "450px", - maxHeight: "85vh", - padding: 25, + maxWidth: "80vh", zIndex: 4, "&:focus": { outline: "none" }, }); +export const introspectTileType = async (file: PMTiles): Promise => { + let magic = await file.source.getBytes(512000, 4); + let b0 = magic.getUint8(0); + let b1 = magic.getUint8(1); + let b2 = magic.getUint8(2); + let b3 = magic.getUint8(3); + + if (b0 == 0x89 && b1 == 0x50 && b2 == 0x4e && b3 == 0x47) { + return TileType.PNG; + } else if (b0 == 0xff && b1 == 0xd8 && b2 == 0xff && b3 == 0xe0) { + return TileType.JPG; + } else if (b0 == 0x1f && b1 == 0x8b) { + return TileType.MVT_GZ; + } else { + return TileType.MVT; + } +}; + const Toolbar = StyledToolbar; -const ToolbarButton = StyledButton; -const ToolbarSeparator = StyledSeparator; const ToolbarLink = StyledLink; const ToolbarToggleGroup = StyledToggleGroup; const ToolbarToggleItem = StyledToggleItem; function Loader(props: { file: PMTiles }) { let [tab, setTab] = useState("inspector"); - let [tileType, setTileType] = useState(null); + let [tileType, setTileType] = useState(TileType.UNKNOWN); let [metadata, setMetadata] = useState<[string, string][]>([]); let [modalOpen, setModalOpen] = useState(false); let view; if (tab === "leaflet") { - view = ; + view = ; } else if (tab === "maplibre") { - view = ; + view = ; } else { view = ; } @@ -138,22 +135,6 @@ function Loader(props: { file: PMTiles }) { tmp.push([key, m[key]]); } setMetadata(tmp); - - let magic = await props.file.source.getBytes(512000, 4); - let b0 = magic.getUint8(0); - let b1 = magic.getUint8(1); - let b2 = magic.getUint8(2); - let b3 = magic.getUint8(3); - - if (b0 == 0x89 && b1 == 0x50 && b2 == 0x4e && b3 == 0x47) { - setTileType("png"); - } else if (b0 == 0xff && b1 == 0xd8 && b2 == 0xff && b3 == 0xe0) { - setTileType("jpg"); - } else if (b0 == 0x1f && b1 == 0x8b) { - setTileType("mvt.gz"); - } else { - setTileType("mvt"); - } }; fetchData(); }, [props.file]); @@ -189,41 +170,36 @@ function Loader(props: { file: PMTiles }) { MapLibre - {props.file.source.getKey()} - setModalOpen(true)} - > - Metadata - + + setModalOpen(true)}> + Metadata + + + + + + + + + + + + + + {metadataRows} +
keyvalue
+ +
+
+
- - - - - - - - - - - - - - - {metadataRows} -
keyvalue
- -
-
-
{view} ); diff --git a/app/src/MaplibreMap.tsx b/app/src/MaplibreMap.tsx index 086542c..97ef6b7 100644 --- a/app/src/MaplibreMap.tsx +++ b/app/src/MaplibreMap.tsx @@ -1,11 +1,12 @@ import { useState, useEffect, useRef } from "react"; import { PMTiles, ProtocolCache } from "../../js"; import { styled } from "./stitches.config"; +import { introspectTileType, TileType } from "./Loader"; import maplibregl from "maplibre-gl"; import "maplibre-gl/dist/maplibre-gl.css"; const MapContainer = styled("div", { - height: "calc(100vh - $4)", + height: "calc(100vh - $4 - $4)", }); const rasterStyle = (file: PMTiles) => { @@ -28,53 +29,81 @@ const rasterStyle = (file: PMTiles) => { }; }; -const vectorStyle = (file: PMTiles) => { +const vectorStyle = async (file: PMTiles): Promise => { + let metadata = await file.metadata(); + let layers: any[] = []; + + if (metadata.json) { + let root = JSON.parse(metadata.json); + if (root.tilestats) { + for (let layer of root.tilestats.layers) { + if (layer.geometry === "Polygon") { + layers.push({ + id: layer.layer + "_fill", + type: "fill", + source: "source", + "source-layer": layer.layer, + paint: { + "fill-color": "white", + }, + }); + } else if (layer.geometry === "LineString") { + layers.push({ + id: layer.layer + "_stroke", + type: "line", + source: "source", + "source-layer": layer.layer, + paint: { + "line-color": "steelblue", + "line-width": 0.5, + }, + }); + } else { + layers.push({ + id: layer.layer + "_point", + type: "circle", + source: "source", + "source-layer": layer.layer, + paint: { + "circle-color": "red", + }, + }); + } + } + } + } + return { version: 8, sources: { source: { type: "vector", tiles: ["pmtiles://" + file.source.getKey() + "/{z}/{x}/{y}"], - maxzoom: 7, + maxzoom: 10, }, }, - layers: [ - { - id: "zcta_fill", - type: "fill", - source: "source", - "source-layer": "zcta", - paint: { - "fill-color": "white", - }, - }, - { - id: "zcta_stroke", - type: "line", - source: "source", - "source-layer": "zcta", - paint: { - "line-color": "steelblue", - "line-width": 0.5, - }, - }, - ], + layers: layers, }; }; -function MaplibreMap(props: { file: PMTiles; tileType: string | null }) { - let mapRef = useRef(null); +function MaplibreMap(props: { file: PMTiles }) { + let mapContainerRef = useRef(null); + let map: maplibregl.Map; + useEffect(() => { let cache = new ProtocolCache(); maplibregl.addProtocol("pmtiles", cache.protocol); - cache.add(props.file); - const map = new maplibregl.Map({ - container: "map", + map = new maplibregl.Map({ + container: mapContainerRef.current!, zoom: 0, center: [0, 0], - style: rasterStyle(props.file) as any, - }); // TODO maplibre types (not any) + style: { + version: 8, + sources: {}, + layers: [], + }, + }); map.addControl(new maplibregl.NavigationControl({})); map.on("load", map.resize); @@ -83,7 +112,24 @@ function MaplibreMap(props: { file: PMTiles; tileType: string | null }) { }; }, []); - return ; + useEffect(() => { + let initStyle = async () => { + if (map) { + let tileType = await introspectTileType(props.file); + let style: any; // TODO maplibre types (not any) + if (tileType === TileType.PNG || tileType == TileType.JPG) { + map.setStyle(rasterStyle(props.file) as any); + } else { + let style = await vectorStyle(props.file); + map.setStyle(style); + } + } + }; + + initStyle(); + }, []); + + return ; } export default MaplibreMap; diff --git a/app/src/Start.tsx b/app/src/Start.tsx index fec725b..67b1d36 100644 --- a/app/src/Start.tsx +++ b/app/src/Start.tsx @@ -129,7 +129,7 @@ function Start(props: { }; const onSubmit = () => { - // props.setFile(new PMTiles("abcd")); + props.setFile(new PMTiles(remoteUrl)); }; return ( diff --git a/app/src/stitches.config.ts b/app/src/stitches.config.ts index 73d562e..15f425a 100644 --- a/app/src/stitches.config.ts +++ b/app/src/stitches.config.ts @@ -5,8 +5,8 @@ export const { styled } = createStitches({ colors: { black: "rgba(0, 0, 0)", white: "rgba(236, 237, 238)", - hover: "#666", - selected: "#444", + hover: "#7180B9", + primary: "#3423A6", }, fonts: { sans: "Inter, sans-serif", @@ -55,7 +55,6 @@ export const globalStyles = globalCss({ padding: 0, border: 0, fontFamily: "Inter, sans-serif", - xborder: "1px solid gold", }, body: { backgroundColor: "$black", color: "$white" }, "@import": ["url('https://rsms.me/inter/inter.css')"], diff --git a/js/index.ts b/js/index.ts index 52dcdf1..575560a 100644 --- a/js/index.ts +++ b/js/index.ts @@ -536,7 +536,7 @@ export class ProtocolCache { instance!.source .getBytes(val.offset, val.length) .then((arr) => { - callback(null, arr, null, null); + callback(null, new Uint8Array(arr.buffer), null, null); }) .catch((e) => { callback(new Error("Canceled"), null, null, null);