/* @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 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;
tileContents?: number;
addressedTiles?: number;
totalEntries?: number;
setHoveredTile: Setter;
setOpenedLeaf: Setter;
}) {
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}
);
}
function ArchiveView(props: { genericTileset: Accessor }) {
const tileset = createMemo(() => {
console.log("memo!");
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();
const [hoveredTile, setHoveredTile] = createSignal();
const [leafEntries] = createResource(openedLeaf, async (o) => {
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 (
{(l) => (
)}
{(h) => (
clustered: {h().clustered ? "true" : "false"}
total addressed tiles: {h().numAddressedTiles}
total tile entries: {h().numTileEntries}
total contents: {h().numTileContents}
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);
}