/* @refresh reload */ import "maplibre-gl/dist/maplibre-gl.css"; import "./index.css"; import { SphericalMercator } from "@mapbox/sphericalmercator"; import { layers, namedFlavor } from "@protomaps/basemaps"; import { AttributionControl, type GeoJSONSource, Map as MaplibreMap, getRTLTextPluginStatus, setRTLTextPlugin, } from "maplibre-gl"; import { Compression, type Entry, tileIdToZxy, tileTypeExt } from "pmtiles"; import { type Accessor, For, type Setter, Show, createEffect, createMemo, createResource, createSignal, onMount, } from "solid-js"; import { render } from "solid-js/web"; import { ExampleChooser, Frame } from "./Frame"; import { PMTilesTileset, type Tileset, tilesetFromString } from "./tileset"; import { createHash, formatBytes, parseHash, tileInspectUrl } from "./utils"; const NONE = Number.MAX_VALUE; const compressionToString = (t: Compression) => { if (t === Compression.Unknown) return "unknown"; if (t === Compression.None) return "none"; if (t === Compression.Gzip) return "gzip"; if (t === Compression.Brotli) return "brotli"; if (t === Compression.Zstd) return "zstd"; return "out of spec"; }; function MapView(props: { entries: Entry[] | undefined; hoveredTile?: number; }) { let mapContainer: HTMLDivElement | undefined; const sp = new SphericalMercator(); let map: MaplibreMap; createEffect(() => { const features = []; const featuresLines = []; if (props.entries) { for (const e of props.entries) { if (e.runLength === 1) { const [z, x, y] = tileIdToZxy(e.tileId); const bbox = sp.bbox(x, y, z); features.push({ type: "Feature" as const, properties: {}, geometry: { type: "Polygon" as const, coordinates: [ [ [bbox[0], bbox[1]], [bbox[2], bbox[1]], [bbox[2], bbox[3]], [bbox[0], bbox[3]], [bbox[0], bbox[1]], ], ], }, }); } else { const coordinates = []; for (let i = e.tileId; i < e.tileId + e.runLength; i++) { const [z, x, y] = tileIdToZxy(i); const bbox = sp.bbox(x, y, z); const midX = (bbox[0] + bbox[2]) / 2; const midY = (bbox[1] + bbox[3]) / 2; coordinates.push([midX, midY]); } featuresLines.push({ type: "Feature" as const, properties: {}, geometry: { type: "LineString" as const, coordinates: coordinates }, }); } } (map.getSource("archive") as GeoJSONSource).setData({ type: "FeatureCollection" as const, features: features, }); (map.getSource("runs") as GeoJSONSource).setData({ type: "FeatureCollection" as const, features: featuresLines, }); } }); createEffect(() => { if (props.hoveredTile) { const [z, x, y] = tileIdToZxy(props.hoveredTile); const bbox = sp.bbox(x, y, z); (map.getSource("hoveredTile") as GeoJSONSource).setData({ type: "Polygon", coordinates: [ [ [bbox[0], bbox[1]], [bbox[2], bbox[1]], [bbox[2], bbox[3]], [bbox[0], bbox[3]], [bbox[0], bbox[1]], ], ], }); map.flyTo({ center: [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2], zoom: Math.max(z - 4, 0), }); } }); onMount(() => { if (!mapContainer) { console.error("Could not mount map element"); return; } if (getRTLTextPluginStatus() === "unavailable") { setRTLTextPlugin( "https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.2.3/mapbox-gl-rtl-text.min.js", true, ); } let flavor = "white"; if (window.matchMedia?.("(prefers-color-scheme: dark)").matches) { flavor = "black"; } map = new MaplibreMap({ container: mapContainer, attributionControl: false, style: { version: 8, glyphs: "https://protomaps.github.io/basemaps-assets/fonts/{fontstack}/{range}.pbf", sprite: `https://protomaps.github.io/basemaps-assets/sprites/v4/${flavor}`, sources: { basemap: { type: "vector", tiles: [ "https://api.protomaps.com/tiles/v4/{z}/{x}/{y}.mvt?key=1003762824b9687f", ], attribution: "© OpenStreetMap", maxzoom: 15, }, archive: { type: "geojson", data: { type: "FeatureCollection", features: [] }, buffer: 16, tolerance: 0, }, runs: { type: "geojson", data: { type: "FeatureCollection", features: [] }, buffer: 16, tolerance: 0, }, hoveredTile: { type: "geojson", data: { type: "FeatureCollection", features: [] }, buffer: 16, tolerance: 0, }, }, layers: [ ...layers("basemap", namedFlavor(flavor), { lang: "en" }), { id: "archive", source: "archive", type: "line", paint: { "line-color": "#3131DC", "line-opacity": 0.8, "line-width": 2, }, }, { id: "runs", source: "runs", type: "line", paint: { "line-color": "#ffffff", "line-opacity": 0.3, }, }, { id: "hoveredTile", source: "hoveredTile", type: "fill", paint: { "fill-color": "white", "fill-opacity": 0.3, }, }, ], }, }); map.addControl(new AttributionControl({ compact: false }), "bottom-right"); map.on("style.load", () => { map.setProjection({ type: "globe", }); map.resize(); }); }); return (
); } function DirectoryTable(props: { entries: Entry[]; stateUrl: string | undefined; setHoveredTile: Setter; setOpenedLeaf: Setter; isLeaf?: boolean; }) { const [idx, setIdx] = createSignal(0); const canNavigate = (targetIdx: number) => { return targetIdx >= 0 && targetIdx < props.entries.length; }; return (
entries {idx()}-{idx() + 999} of {props.entries.length}
{(e) => ( props.setHoveredTile(e.tileId)} > )}
tileID z x y offset length runlength
{e.tileId} {tileIdToZxy(e.tileId)[0]} {tileIdToZxy(e.tileId)[1]} {tileIdToZxy(e.tileId)[2]} {e.offset} } > {e.length} 0 (leaf)
); } function ArchiveView(props: { genericTileset: Accessor }) { const tileset = createMemo(() => { const g = props.genericTileset(); if (g instanceof PMTilesTileset) { return g as PMTilesTileset; } alert("This isn't a PMTiles archive!"); throw "This isn't a PMTiles tileset"; }); const [header] = createResource(tileset, async (t) => { return await t.archive.getHeader(); }); const [rootEntries] = createResource(header, async (h) => { return await tileset().archive.cache.getDirectory( tileset().archive.source, h.rootDirectoryOffset, h.rootDirectoryLength, h, ); }); const [openedLeaf, setOpenedLeaf] = createSignal(NONE); const [hoveredTile, setHoveredTile] = createSignal(); const [leafEntries] = createResource(openedLeaf, async (o) => { if (o === NONE) return; const h = header(); const root = rootEntries(); if (!root) return; if (!h) return; const found = root.find((e) => e.tileId === o); if (!found) return; return await tileset().archive.cache.getDirectory( tileset().archive.source, h.leafDirectoryOffset + found.offset, found.length, h, ); }); return (
{(h) => (
Layout (bytes) offset length
Root Dir {h().rootDirectoryOffset} {formatBytes(h().rootDirectoryLength)}
Metadata {h().jsonMetadataOffset} {formatBytes(h().jsonMetadataLength)}
Leaf Dirs {h().leafDirectoryOffset} {formatBytes(h().leafDirectoryLength || 0)}
Tile Data {h().tileDataOffset} {formatBytes(h().tileDataLength || 0)}
)}
{(l) => (
)}
{(h) => (
Addressed tiles {h().numAddressedTiles.toLocaleString()}
Tile entries {h().numTileEntries.toLocaleString()}
Tile contents {h().numTileContents.toLocaleString()}
Clustered {h().clustered ? "true" : "false"}
Internal compression {compressionToString(h().internalCompression)}
Tile compression {compressionToString(h().tileCompression)}
Tile type {tileTypeExt(h().tileType)}
Min zoom {h().minZoom}
Max zoom {h().maxZoom}
Center zoom {h().centerZoom}
Bounds {h().minLon} {h().minLat} {h().maxLon} {h().maxLat}
Center {h().centerLon} {h().centerLat}
)}
); } function PageArchive() { const hash = parseHash(location.hash); const [tileset, setTileset] = createSignal( hash.url ? tilesetFromString(decodeURIComponent(hash.url)) : undefined, ); createEffect(() => { const t = tileset(); const stateUrl = t?.getStateUrl(); location.hash = createHash(location.hash, { url: stateUrl ? encodeURIComponent(stateUrl) : undefined, }); }); return ( } > {(t) => } ); } const root = document.getElementById("root"); if (root) { render(() => , root); }