diff --git a/app/src/PageArchive.tsx b/app/src/PageArchive.tsx index e2f7117..5987709 100644 --- a/app/src/PageArchive.tsx +++ b/app/src/PageArchive.tsx @@ -12,10 +12,12 @@ import { } from "maplibre-gl"; import { Compression, type Entry, tileIdToZxy, tileTypeExt } from "pmtiles"; import { + type Accessor, For, type Setter, Show, createEffect, + createMemo, createResource, createSignal, onMount, @@ -343,16 +345,18 @@ function DirectoryTable(props: { ); } -function ArchiveView(props: { genericTileset: Tileset }) { - const tileset = () => { - if (props.genericTileset instanceof PMTilesTileset) { - return props.genericTileset as PMTilesTileset; +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) => { + const [header] = createResource(tileset, async (t) => { return await t.archive.getHeader(); }); @@ -438,7 +442,7 @@ function ArchiveView(props: { genericTileset: Tileset }) { @@ -449,7 +453,7 @@ function ArchiveView(props: { genericTileset: Tileset }) {
@@ -521,7 +525,7 @@ function PageArchive() { when={tileset()} fallback={} > - {(t) => } + {(t) => } ); diff --git a/app/src/PageMap.tsx b/app/src/PageMap.tsx index c732123..48bb10e 100644 --- a/app/src/PageMap.tsx +++ b/app/src/PageMap.tsx @@ -41,7 +41,7 @@ declare module "solid-js" { } function MapView(props: { - tileset: Tileset; + tileset: Accessor; showMetadata: Accessor; setShowMetadata: Setter; showTileBoundaries: Accessor; @@ -83,6 +83,164 @@ function MapView(props: { addProtocol("pmtiles", protocol.tile); let map: MaplibreMap; + let initialLoad = true; + + const roundZoom = () => { + map.zoomTo(Math.round(map.getZoom())); + }; + + const fitToBounds = async () => { + const bounds = await props.tileset().getBounds(); + map.fitBounds( + [ + [bounds[0], bounds[1]], + [bounds[2], bounds[3]], + ], + { animate: false }, + ); + }; + + const removeTileset = () => { + for (const layer of map.getStyle().layers) { + if ("source" in layer && layer.source === "tileset") { + map.removeLayer(layer.id); + } + } + map.removeSource("tileset"); + }; + + const addTileset = async (tileset: Tileset) => { + const archiveForProtocol = tileset.archiveForProtocol(); + if (archiveForProtocol) { + protocol.add(archiveForProtocol); + } + let flavor = "white"; + if (window.matchMedia?.("(prefers-color-scheme: dark)").matches) { + flavor = "black"; + } + if (await tileset.isOverlay()) { + setBasemap(true); + } + + if (await tileset.isVector()) { + map.addSource("tileset", { + type: "vector", + url: tileset.getMaplibreSourceUrl(), + }); + const vectorLayers = await tileset.getVectorLayers(); + setLayerVisibility(vectorLayers.map((v) => ({ id: v, visible: true }))); + for (const [i, vectorLayer] of vectorLayers.entries()) { + map.addLayer({ + id: `tileset_fill_${vectorLayer}`, + type: "fill", + source: "tileset", + "source-layer": vectorLayer, + paint: { + "fill-color": colorForIdx(i), + "fill-opacity": [ + "case", + ["boolean", ["feature-state", "hover"], false], + 0.25, + 0.1, + ], + }, + filter: ["==", ["geometry-type"], "Polygon"], + }); + map.addLayer({ + id: `tileset_line_${vectorLayer}`, + type: "line", + source: "tileset", + "source-layer": vectorLayer, + paint: { + "line-color": colorForIdx(i), + "line-width": [ + "case", + ["boolean", ["feature-state", "hover"], false], + 2, + 0.5, + ], + }, + filter: ["==", ["geometry-type"], "LineString"], + }); + map.addLayer({ + id: `tileset_circle_${vectorLayer}`, + type: "circle", + source: "tileset", + "source-layer": vectorLayer, + paint: { + "circle-color": colorForIdx(i), + "circle-radius": 3, + "circle-stroke-color": "white", + "circle-stroke-width": [ + "case", + ["boolean", ["feature-state", "hover"], false], + 3, + 0, + ], + }, + filter: ["==", ["geometry-type"], "Point"], + }); + } + for (const [i, vectorLayer] of vectorLayers.entries()) { + map.addLayer({ + id: `tileset_line_label_${vectorLayer}`, + type: "symbol", + source: "tileset", + "source-layer": vectorLayer, + layout: { + "text-field": ["get", "name"], + "text-font": ["Noto Sans Regular"], + "text-size": 10, + "symbol-placement": "line", + }, + paint: { + "text-color": colorForIdx(i), + "text-halo-color": flavor, + "text-halo-width": 2, + }, + filter: ["==", ["geometry-type"], "LineString"], + }); + map.addLayer({ + id: `tileset_point_label_${vectorLayer}`, + type: "symbol", + source: "tileset", + "source-layer": vectorLayer, + layout: { + "text-field": ["get", "name"], + "text-font": ["Noto Sans Regular"], + "text-size": 10, + "text-offset": [0, -1], + }, + paint: { + "text-color": colorForIdx(i), + "text-halo-color": flavor, + "text-halo-width": 2, + }, + filter: ["==", ["geometry-type"], "Point"], + }); + } + } else { + map.addSource("tileset", { + type: "raster", + url: tileset.getMaplibreSourceUrl(), + }); + map.addLayer({ + source: "tileset", + id: "tileset_raster", + type: "raster", + }); + } + }; + + createEffect(() => { + const tileset = props.tileset(); + if (initialLoad) { + initialLoad = false; + return; + } + removeTileset(); + addTileset(tileset); + }); createEffect(() => { const visibility = basemap() ? "visible" : "none"; @@ -95,9 +253,39 @@ function MapView(props: { } }); - const roundZoom = () => { - map.zoomTo(Math.round(map.getZoom())); - }; + createEffect(() => { + const show = props.showTileBoundaries(); + if (map) { + map.showTileBoundaries = show; + } + }); + + createEffect(() => { + if (props.inspectFeatures()) { + setFrozen(false); + } else { + for (const hoveredFeature of hoveredFeatures()) { + map.setFeatureState(hoveredFeature, { hover: false }); + } + popup.remove(); + } + }); + + createEffect(() => { + const setVisibility = (layerName: string, visibility: string) => { + if (map.getLayer(layerName)) { + map.setLayoutProperty(layerName, "visibility", visibility); + } + }; + + for (const { id, visible } of layerVisibility()) { + const visibility = visible ? "visible" : "none"; + setVisibility(`tileset_fill_${id}`, visibility); + setVisibility(`tileset_line_${id}`, visibility); + setVisibility(`tileset_circle_${id}`, visibility); + setVisibility(`tileset_point_label_${id}`, visibility); + } + }); onMount(async () => { if (!mapContainer) { @@ -105,11 +293,6 @@ function MapView(props: { return; } - const archiveForProtocol = props.tileset.archiveForProtocol(); - if (archiveForProtocol) { - protocol.add(archiveForProtocol); - } - if (getRTLTextPluginStatus() === "unavailable") { setRTLTextPlugin( "https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.2.3/mapbox-gl-rtl-text.min.js", @@ -154,21 +337,6 @@ function MapView(props: { }, }); - createEffect(() => { - map.showTileBoundaries = props.showTileBoundaries(); - }); - - createEffect(() => { - if (props.inspectFeatures()) { - setFrozen(false); - } else { - for (const hoveredFeature of hoveredFeatures()) { - map.setFeatureState(hoveredFeature, { hover: false }); - } - popup.remove(); - } - }); - map.addControl(new NavigationControl({}), "top-left"); map.addControl(new AttributionControl({ compact: false }), "bottom-right"); @@ -206,7 +374,7 @@ function MapView(props: { const currentZoom = zoom(); const sp = new SphericalMercator(); - const maxZoom = await props.tileset.getMaxZoom(); + const maxZoom = await props.tileset().getMaxZoom(); const z = Math.max(0, Math.min(maxZoom, Math.floor(currentZoom))); const result = sp.px([e.lngLat.lng, e.lngLat.lat], z); const tileX = Math.floor(result[0] / 256); @@ -222,7 +390,7 @@ function MapView(props: { class="block text-xs btn-primary mt-2 text-center" target="_blank" rel="noreferrer" - href={tileInspectUrl(props.tileset.getStateUrl(), [ + href={tileInspectUrl(props.tileset().getStateUrl(), [ z, tileX, tileY, @@ -245,131 +413,11 @@ function MapView(props: { }); map.on("load", async () => { - if (await props.tileset.isOverlay()) { - setBasemap(true); - } - - if (await props.tileset.isVector()) { - map.addSource("tileset", { - type: "vector", - url: props.tileset.getMaplibreSourceUrl(), - }); - const vectorLayers = await props.tileset.getVectorLayers(); - setLayerVisibility(vectorLayers.map((v) => ({ id: v, visible: true }))); - for (const [i, vectorLayer] of vectorLayers.entries()) { - map.addLayer({ - id: `tileset_fill_${vectorLayer}`, - type: "fill", - source: "tileset", - "source-layer": vectorLayer, - paint: { - "fill-color": colorForIdx(i), - "fill-opacity": [ - "case", - ["boolean", ["feature-state", "hover"], false], - 0.25, - 0.1, - ], - }, - filter: ["==", ["geometry-type"], "Polygon"], - }); - map.addLayer({ - id: `tileset_line_${vectorLayer}`, - type: "line", - source: "tileset", - "source-layer": vectorLayer, - paint: { - "line-color": colorForIdx(i), - "line-width": [ - "case", - ["boolean", ["feature-state", "hover"], false], - 2, - 0.5, - ], - }, - filter: ["==", ["geometry-type"], "LineString"], - }); - map.addLayer({ - id: `tileset_circle_${vectorLayer}`, - type: "circle", - source: "tileset", - "source-layer": vectorLayer, - paint: { - "circle-color": colorForIdx(i), - "circle-radius": 3, - "circle-stroke-color": "white", - "circle-stroke-width": [ - "case", - ["boolean", ["feature-state", "hover"], false], - 3, - 0, - ], - }, - filter: ["==", ["geometry-type"], "Point"], - }); - } - for (const [i, vectorLayer] of vectorLayers.entries()) { - map.addLayer({ - id: `tileset_point_label_${vectorLayer}`, - type: "symbol", - source: "tileset", - "source-layer": vectorLayer, - layout: { - "text-field": ["get", "name"], - "text-font": ["Noto Sans Regular"], - "text-size": 10, - "text-offset": [0, -1], - }, - paint: { - "text-color": colorForIdx(i), - "text-halo-color": flavor, - "text-halo-width": 2, - }, - filter: ["==", ["geometry-type"], "Point"], - }); - } - } else { - map.addSource("tileset", { - type: "raster", - url: props.tileset.getMaplibreSourceUrl(), - }); - map.addLayer({ - source: "tileset", - id: "tileset_raster", - type: "raster", - }); - } + await addTileset(props.tileset()); map.resize(); }); }); - const fitToBounds = async () => { - const bounds = await props.tileset.getBounds(); - map.fitBounds( - [ - [bounds[0], bounds[1]], - [bounds[2], bounds[3]], - ], - { animate: false }, - ); - }; - - createEffect(() => { - const setVisibility = (layerName: string, visibility: string) => { - if (map.getLayer(layerName)) { - map.setLayoutProperty(layerName, "visibility", visibility); - } - }; - - for (const { id, visible } of layerVisibility()) { - const visibility = visible ? "visible" : "none"; - setVisibility(`tileset_fill_${id}`, visibility); - setVisibility(`tileset_line_${id}`, visibility); - setVisibility(`tileset_circle_${id}`, visibility); - setVisibility(`tileset_point_label_${id}`, visibility); - } - }); - return (
@@ -458,9 +506,9 @@ function MapView(props: { ); } -const JsonView = (props: { tileset: Tileset }) => { +const JsonView = (props: { tileset: Accessor }) => { const [data] = createResource(async () => { - return await props.tileset.getMetadata(); + return await props.tileset().getMetadata(); }); return ; @@ -517,7 +565,7 @@ function PageMap() { > {(t) => ( ; - tileset: Tileset; + tileset: Accessor; }) { let containerRef: HTMLDivElement | undefined; let svg: Selection; @@ -211,20 +212,27 @@ function ZoomableTile(props: { } }); - const [parsedTile] = createResource(props.zxy, async (zxy) => { - const tileset = props.tileset; + const inputs = createMemo(() => ({ + zxy: props.zxy(), + tileset: props.tileset(), + })); + + const [parsedTile] = createResource(inputs, async (i) => { + const tileset = i.tileset; + const zxy = i.zxy; if (await tileset.isVector()) { const data = await tileset.getZxy(zxy[0], zxy[1], zxy[2]); if (!data) return; - const vectorLayers = await props.tileset.getVectorLayers(); + const vectorLayers = await props.tileset().getVectorLayers(); return parseTile(data, vectorLayers); } return await tileset.getZxy(zxy[0], zxy[1], zxy[2]); }); - onMount(async () => { - if (await props.tileset.isVector()) { - const vectorLayers = await props.tileset.getVectorLayers(); + createEffect(async () => { + const tileset = props.tileset(); + if (await tileset.isVector()) { + const vectorLayers = await tileset.getVectorLayers(); setLayerVisibility(vectorLayers.map((v) => ({ id: v, visible: true }))); } }); @@ -315,7 +323,7 @@ function ZoomableTile(props: { } function TileView(props: { - tileset: Tileset; + tileset: Accessor; zxy: Accessor<[number, number, number] | undefined>; setZxy: Setter<[number, number, number] | undefined>; }) { @@ -523,7 +531,7 @@ function PageTile() { when={tileset()} fallback={} > - {(t) => } + {(t) => } );