inspector app uses new JS client

This commit is contained in:
Brandon Liu
2022-10-05 10:48:00 +08:00
parent f831dd113a
commit d8851a7041
5 changed files with 114 additions and 104 deletions

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, Dispatch, SetStateAction } from "react"; import { useState, useEffect, Dispatch, SetStateAction } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { PMTiles, Entry } from "../../js"; import { PMTiles, Entry, tileIdToZxy, TileType } from "../../js";
import { styled } from "./stitches.config"; import { styled } from "./stitches.config";
import { decompressSync } from "fflate"; import { decompressSync } from "fflate";
import Protobuf from "pbf"; import Protobuf from "pbf";
@@ -8,7 +8,6 @@ import { VectorTile, VectorTileFeature } from "@mapbox/vector-tile";
import { path } from "d3-path"; import { path } from "d3-path";
import { schemeSet3 } from "d3-scale-chromatic"; import { schemeSet3 } from "d3-scale-chromatic";
import * as DialogPrimitive from "@radix-ui/react-dialog"; import * as DialogPrimitive from "@radix-ui/react-dialog";
import { introspectTileType, TileType } from "./Loader";
const TableContainer = styled("div", { const TableContainer = styled("div", {
height: "calc(100vh - $4 - $4)", height: "calc(100vh - $4 - $4)",
@@ -44,18 +43,20 @@ const TileRow = (props: {
entry: Entry; entry: Entry;
setSelectedEntry: Dispatch<SetStateAction<Entry | null>>; setSelectedEntry: Dispatch<SetStateAction<Entry | null>>;
}) => { }) => {
let [z,x,y] = tileIdToZxy(props.entry.tileId);
return ( return (
<TableRow <TableRow
onClick={() => { onClick={() => {
props.setSelectedEntry(props.entry); props.setSelectedEntry(props.entry);
}} }}
> >
<td>{props.entry.z}</td> <td>{props.entry.tileId}</td>
<td>{props.entry.x}</td> <td>{z}</td>
<td>{props.entry.y}</td> <td>{x}</td>
<td>{y}</td>
<td>{props.entry.offset}</td> <td>{props.entry.offset}</td>
<td>{props.entry.length}</td> <td>{props.entry.length}</td>
<td>{props.entry.is_dir}</td> <td>{props.entry.runLength}</td>
</TableRow> </TableRow>
); );
}; };
@@ -178,14 +179,12 @@ const VectorPreview = (props: {
useEffect(() => { useEffect(() => {
let fn = async (entry: Entry) => { let fn = async (entry: Entry) => {
let view = await props.file.source.getBytes(entry.offset, entry.length); let [z,x,y] = tileIdToZxy(entry.tileId);
if (props.tileType == TileType.MVT_GZ) { let resp = await props.file.getZxy(z,x,y);
view = new DataView(decompressSync(new Uint8Array(view.buffer)).buffer);
}
let tile = new VectorTile( let tile = new VectorTile(
new Protobuf( new Protobuf(
new Uint8Array(view.buffer, view.byteOffset, view.byteLength) new Uint8Array(resp!.data)
) )
); );
let newLayers = []; let newLayers = [];
@@ -257,8 +256,9 @@ const RasterPreview = (props: { file: PMTiles; entry: Entry }) => {
useEffect(() => { useEffect(() => {
let fn = async (entry: Entry) => { let fn = async (entry: Entry) => {
// TODO 0,0,0 is broken // TODO 0,0,0 is broken
let view = await props.file.source.getBytes(entry.offset, entry.length); let [z,x,y] = tileIdToZxy(entry.tileId);
let blob = new Blob([view]); let resp = await props.file.getZxy(z,x,y);
let blob = new Blob([resp!.data]);
var imageUrl = window.URL.createObjectURL(blob); var imageUrl = window.URL.createObjectURL(blob);
setImageSrc(imageUrl); setImageSrc(imageUrl);
}; };
@@ -274,14 +274,14 @@ const RasterPreview = (props: { file: PMTiles; entry: Entry }) => {
function Inspector(props: { file: PMTiles }) { function Inspector(props: { file: PMTiles }) {
let [entryRows, setEntryRows] = useState<Entry[]>([]); let [entryRows, setEntryRows] = useState<Entry[]>([]);
let [selectedEntry, setSelectedEntry] = useState<Entry | null>(null); let [selectedEntry, setSelectedEntry] = useState<Entry | null>(null);
let [tileType, setTileType] = useState<TileType>(TileType.UNKNOWN); let [tileType, setTileType] = useState<TileType>(TileType.Unknown);
useEffect(() => { useEffect(() => {
let fn = async () => { let fn = async () => {
let header = await props.file.getHeader();
let entries = await props.file.root_entries(); let entries = await props.file.root_entries();
let tileType = await introspectTileType(props.file);
setEntryRows(entries); setEntryRows(entries);
setTileType(tileType); setTileType(header.tileType);
}; };
fn(); fn();
@@ -293,7 +293,7 @@ function Inspector(props: { file: PMTiles }) {
let tilePreview = <div></div>; let tilePreview = <div></div>;
if (selectedEntry && tileType) { if (selectedEntry && tileType) {
if (tileType === TileType.MVT || tileType === TileType.MVT_GZ) { if (tileType === TileType.Mvt) {
tilePreview = ( tilePreview = (
<VectorPreview <VectorPreview
file={props.file} file={props.file}

View File

@@ -1,8 +1,8 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { PMTiles, leafletLayer as rasterLeafletLayer } from "../../js"; import { PMTiles, TileType } from "../../js";
import { leafletRasterLayer } from "../../js/adapters";
import { leafletLayer as vectorLeafletLayer } from "protomaps"; import { leafletLayer as vectorLeafletLayer } from "protomaps";
import { styled } from "./stitches.config"; import { styled } from "./stitches.config";
import { introspectTileType, TileType } from "./Loader";
import L from "leaflet"; import L from "leaflet";
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
@@ -25,9 +25,9 @@ function LeafletMap(props: { file: PMTiles }) {
if (currentLayer) currentLayer.remove(); if (currentLayer) currentLayer.remove();
let initStyle = async () => { let initStyle = async () => {
if (map) { if (map) {
let tileType = await introspectTileType(props.file); let header = await props.file.getHeader();
if (tileType === TileType.PNG || tileType == TileType.JPG) { if (header.tileType === TileType.Png || header.tileType == TileType.Jpeg) {
currentLayer = rasterLeafletLayer(props.file, { currentLayer = leafletRasterLayer(props.file, {
attribution: attribution:
'© <a href="https://openstreetmap.org">OpenStreetMap</a> contributors', '© <a href="https://openstreetmap.org">OpenStreetMap</a> contributors',
}); });

View File

@@ -10,14 +10,6 @@ import { MagnifyingGlassIcon, ImageIcon } from "@radix-ui/react-icons";
import * as ToolbarPrimitive from "@radix-ui/react-toolbar"; import * as ToolbarPrimitive from "@radix-ui/react-toolbar";
import * as DialogPrimitive from "@radix-ui/react-dialog"; import * as DialogPrimitive from "@radix-ui/react-dialog";
export enum TileType {
UNKNOWN = 1,
PNG,
JPG,
MVT,
MVT_GZ,
}
const StyledToolbar = styled(ToolbarPrimitive.Root, { const StyledToolbar = styled(ToolbarPrimitive.Root, {
display: "flex", display: "flex",
height: "$4", height: "$4",
@@ -113,25 +105,6 @@ const JsonValue = styled(MetadataValue, {
overflowX:"scroll" overflowX:"scroll"
}) })
export const introspectTileType = async (file: PMTiles): Promise<TileType> => {
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 Toolbar = StyledToolbar;
const ToolbarLink = StyledLink; const ToolbarLink = StyledLink;
const ToolbarToggleGroup = StyledToggleGroup; const ToolbarToggleGroup = StyledToggleGroup;
@@ -139,7 +112,6 @@ const ToolbarToggleItem = StyledToggleItem;
function Loader(props: { file: PMTiles }) { function Loader(props: { file: PMTiles }) {
let [tab, setTab] = useState("inspector"); let [tab, setTab] = useState("inspector");
let [tileType, setTileType] = useState<TileType>(TileType.UNKNOWN);
let [metadata, setMetadata] = useState<[string, string][]>([]); let [metadata, setMetadata] = useState<[string, string][]>([]);
let [modalOpen, setModalOpen] = useState<boolean>(false); let [modalOpen, setModalOpen] = useState<boolean>(false);
@@ -155,7 +127,7 @@ function Loader(props: { file: PMTiles }) {
useEffect(() => { useEffect(() => {
let pmtiles = props.file; let pmtiles = props.file;
const fetchData = async () => { const fetchData = async () => {
let m = await pmtiles.metadata(); let m = await pmtiles.getMetadata();
let tmp: [string, string][] = []; let tmp: [string, string][] = [];
for (var key in m) { for (var key in m) {
tmp.push([key, m[key]]); tmp.push([key, m[key]]);

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { PMTiles, ProtocolCache } from "../../js"; import { PMTiles, TileType } from "../../js";
import { Protocol } from "../../js/adapters"
import { styled } from "./stitches.config"; import { styled } from "./stitches.config";
import { introspectTileType, TileType } from "./Loader";
import maplibregl from "maplibre-gl"; import maplibregl from "maplibre-gl";
import "maplibre-gl/dist/maplibre-gl.css"; import "maplibre-gl/dist/maplibre-gl.css";
@@ -30,13 +30,21 @@ const rasterStyle = (file: PMTiles) => {
}; };
const vectorStyle = async (file: PMTiles): Promise<any> => { const vectorStyle = async (file: PMTiles): Promise<any> => {
let metadata = await file.metadata(); let header = await file.getHeader();
let metadata = await file.getMetadata();
let layers: any[] = []; let layers: any[] = [];
var tilestats:any;
if (metadata.json) { if (metadata.json) {
let root = JSON.parse(metadata.json); let j = JSON.parse(metadata.json);
if (root.tilestats) { tilestats = j.tilestats;
for (let layer of root.tilestats.layers) { } else {
tilestats = metadata.tilestats;
}
if (tilestats) {
for (let layer of tilestats.layers) {
if (layer.geometry === "Polygon") { if (layer.geometry === "Polygon") {
layers.push({ layers.push({
id: layer.layer + "_fill", id: layer.layer + "_fill",
@@ -70,7 +78,35 @@ const vectorStyle = async (file: PMTiles): Promise<any> => {
}); });
} }
} }
} else {
layers.push({
id:"water",
type:"fill",
source:"source",
"source-layer":"water",
paint: {
"fill-color":"blue"
} }
})
layers.push({
id:"landuse",
type:"fill",
source:"source",
"source-layer":"landuse",
paint: {
"fill-color":"green"
}
})
layers.push({
id:"roads",
type:"line",
source:"source",
"source-layer":"roads",
paint: {
"line-color":"white",
"line-width":0.5
}
})
} }
return { return {
@@ -79,7 +115,8 @@ const vectorStyle = async (file: PMTiles): Promise<any> => {
source: { source: {
type: "vector", type: "vector",
tiles: ["pmtiles://" + file.source.getKey() + "/{z}/{x}/{y}"], tiles: ["pmtiles://" + file.source.getKey() + "/{z}/{x}/{y}"],
maxzoom: 10, minzoom: header.minZoom,
maxzoom: header.maxZoom
}, },
}, },
layers: layers, layers: layers,
@@ -91,14 +128,15 @@ function MaplibreMap(props: { file: PMTiles }) {
let map: maplibregl.Map; let map: maplibregl.Map;
useEffect(() => { useEffect(() => {
let cache = new ProtocolCache(); let protocol = new Protocol();
maplibregl.addProtocol("pmtiles", cache.protocol); maplibregl.addProtocol("pmtiles", protocol.tileFunc);
cache.add(props.file); // this is necessary for non-HTTP sources protocol.add(props.file); // this is necessary for non-HTTP sources
map = new maplibregl.Map({ map = new maplibregl.Map({
container: mapContainerRef.current!, container: mapContainerRef.current!,
hash: true,
zoom: 0, zoom: 0,
center: [0, 0], center: [0,0],
style: { style: {
version: 8, version: 8,
sources: {}, sources: {},
@@ -116,15 +154,14 @@ function MaplibreMap(props: { file: PMTiles }) {
useEffect(() => { useEffect(() => {
let initStyle = async () => { let initStyle = async () => {
if (map) { if (map) {
let metadata = await props.file.metadata(); let header = await props.file.getHeader();
let bounds = metadata.bounds.split(",");
map.fitBounds([ // map.fitBounds([
[+bounds[0], +bounds[1]], // [header.minLon, header.minLat],
[+bounds[2], +bounds[3]], // [header.maxLon, header.maxLat],
]); // ]);
let tileType = await introspectTileType(props.file);
let style: any; // TODO maplibre types (not any) let style: any; // TODO maplibre types (not any)
if (tileType === TileType.PNG || tileType == TileType.JPG) { if (header.tileType === TileType.Png || header.tileType == TileType.Jpeg) {
map.setStyle(rasterStyle(props.file) as any); map.setStyle(rasterStyle(props.file) as any);
} else { } else {
let style = await vectorStyle(props.file); let style = await vectorStyle(props.file);

View File

@@ -1,7 +1,7 @@
import { useState, Dispatch, SetStateAction, useCallback } from "react"; import { useState, Dispatch, SetStateAction, useCallback } from "react";
import maplibregl from "maplibre-gl"; import maplibregl from "maplibre-gl";
import L from "leaflet"; import L from "leaflet";
import { PMTiles, FileSource } from "../../js"; import { PMTiles, FileAPISource } from "../../js";
import { styled } from "./stitches.config"; import { styled } from "./stitches.config";
import { useDropzone } from "react-dropzone"; import { useDropzone } from "react-dropzone";
@@ -107,8 +107,9 @@ const EXAMPLE_FILES = [
function Start(props: { function Start(props: {
setFile: Dispatch<SetStateAction<PMTiles | undefined>>; setFile: Dispatch<SetStateAction<PMTiles | undefined>>;
}) { }) {
const onDrop = useCallback((acceptedFiles: File[]) => { const onDrop = useCallback((acceptedFiles: File[]) => {
props.setFile(new PMTiles(new FileSource(acceptedFiles[0]))); props.setFile(new PMTiles(new FileAPISource(acceptedFiles[0])));
}, []); }, []);
const { acceptedFiles, getRootProps, getInputProps } = useDropzone({ const { acceptedFiles, getRootProps, getInputProps } = useDropzone({