diff --git a/app/src/MapView.tsx b/app/src/MapView.tsx index fea945f0..103f98b8 100644 --- a/app/src/MapView.tsx +++ b/app/src/MapView.tsx @@ -42,6 +42,17 @@ import { parseHash, } from "./utils"; +const STYLE_MAJOR_VERSION = 4; + +const DEFAULT_TILES = "https://demo-bucket.protomaps.com/v4.pmtiles"; + +const VERSION_COMPATIBILITY: Record = { + 4: [4], + 3: [3], + 2: [2], + 1: [1], +}; + const ATTRIBUTION = 'Protomaps © OpenStreetMap'; @@ -181,7 +192,7 @@ function StyleJsonPane(props: { theme: string; lang: string }) { }); return ( -
+
-
@@ -207,18 +221,20 @@ function MapLibreView(props: { showBoxes: boolean; tiles?: string; npmLayers: LayerSpecification[]; + npmVersion?: string; droppedArchive?: PMTiles; ref?: (ref: MapLibreViewRef) => void; }) { let mapContainer: HTMLDivElement | undefined; let mapRef: MaplibreMap | undefined; - let protocolRef: Protocol | undefined; let hiddenRef: HTMLDivElement | undefined; let longPressTimeout: ReturnType; const [error, setError] = createSignal(); const [timelinessInfo, setTimelinessInfo] = createSignal(); + const [protocolRef, setProtocolRef] = createSignal(); const [zoom, setZoom] = createSignal(0); + const [mismatch, setMismatch] = createSignal(""); onMount(() => { props.ref?.({ fit }); @@ -236,7 +252,7 @@ function MapLibreView(props: { } const protocol = new Protocol({ metadata: true }); - protocolRef = protocol; + setProtocolRef(protocol); addProtocol("pmtiles", protocol.tile); const map = new MaplibreMap({ @@ -288,9 +304,9 @@ function MapLibreView(props: { map.on("idle", () => { setZoom(map.getZoom()); setError(undefined); - if (protocolRef && props.tiles) { - const p = protocolRef.tiles.get(props.tiles); - p?.getMetadata().then((metadata) => { + memoizedArchive() + ?.getMetadata() + .then((metadata) => { if (metadata) { const m = metadata as { version?: string; @@ -301,7 +317,6 @@ function MapLibreView(props: { ); } }); - } }); const showContextMenu = (e: MapTouchEvent) => { @@ -348,58 +363,87 @@ function MapLibreView(props: { mapRef = map; return () => { - protocolRef = undefined; + setProtocolRef(undefined); removeProtocol("pmtiles"); map.remove(); }; }); - const fit = async () => { - if (protocolRef) { - let archive = props.droppedArchive; - if (!archive && props.tiles) { - archive = new PMTiles(props.tiles); - protocolRef.add(archive); + // ensure the dropped archive is first added to the protocol + const memoizedArchive = () => { + const p = protocolRef(); + if (p) { + if (props.droppedArchive) { + p.add(props.droppedArchive); + return props.droppedArchive; } - if (archive) { - const header = await archive.getHeader(); - mapRef?.fitBounds( - [ - [header.minLon, header.minLat], - [header.maxLon, header.maxLat], - ], - { animate: false }, - ); + if (props.tiles) { + let archive = p.tiles.get(props.tiles); + if (!archive) { + archive = new PMTiles(props.tiles); + p.add(archive); + } + return archive; } } }; - createEffect(() => { - if (mapRef) { - mapRef.showTileBoundaries = props.showBoxes; - mapRef.showCollisionBoxes = props.showBoxes; + const fit = async () => { + const header = await memoizedArchive()?.getHeader(); + if (header) { + mapRef?.fitBounds( + [ + [header.minLon, header.minLat], + [header.maxLon, header.maxLat], + ], + { animate: false }, + ); } + }; + + const memoizedStyle = createMemo(() => { + return getMaplibreStyle( + props.theme, + props.lang, + props.localSprites, + props.tiles, + props.npmLayers, + props.droppedArchive, + ); }); createEffect(() => { - // ensure the dropped archive is first added to the protocol - if (protocolRef && props.droppedArchive) { - protocolRef.add(props.droppedArchive); + const styleMajorVersion = props.npmVersion + ? +props.npmVersion.split(".")[0] + : STYLE_MAJOR_VERSION; + memoizedArchive() + ?.getMetadata() + .then((m) => { + if (m instanceof Object && "version" in m) { + const tilesetVersion = +(m.version as string).split(".")[0]; + if ( + VERSION_COMPATIBILITY[tilesetVersion].indexOf(styleMajorVersion) < 0 + ) { + setMismatch( + `style v${styleMajorVersion} may not be compatible with tileset v${tilesetVersion}. `, + ); + } else { + setMismatch(""); + } + } + }); + }); + + createEffect(() => { + if (mapRef) { + mapRef.showTileBoundaries = props.showBoxes; + mapRef.showCollisionBoxes = props.showBoxes; } }); createEffect(() => { if (mapRef) { - mapRef.setStyle( - getMaplibreStyle( - props.theme, - props.lang, - props.localSprites, - props.tiles, - props.npmLayers, - props.droppedArchive, - ), - ); + mapRef.setStyle(memoizedStyle()); } }); @@ -408,11 +452,22 @@ function MapLibreView(props: {