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}
+
+
+ );
};
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
+
+
+
+
+
+
+
+
+
+ | key |
+ value |
+
+
+ {metadataRows}
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
- | key |
- value |
-
-
- {metadataRows}
-
-
-
-
-
{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);