Offline-capable geomap, meant for storing location bookmarks
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: lazy-load region maps

+91 -35
+91 -35
www/components/m-map.ts
··· 20 20 21 21 const registeredSources = new Set<string>() 22 22 23 + // World tiles cover lower zooms; regional sources only pull their weight 24 + // at street-level detail, so keep them out of the style until then. 25 + const MIN_REGIONAL_ZOOM = 6 26 + const VIEWPORT_BUFFER = 0.5 27 + 28 + function expandBounds( 29 + bounds: maplibregl.LngLatBounds, 30 + factor: number, 31 + ): [number, number, number, number] { 32 + const w = bounds.getWest() 33 + const s = bounds.getSouth() 34 + const e = bounds.getEast() 35 + const n = bounds.getNorth() 36 + const dx = (e - w) * factor / 2 37 + const dy = (n - s) * factor / 2 38 + return [w - dx, s - dy, e + dx, n + dy] 39 + } 40 + 41 + function boundsIntersect( 42 + a: [number, number, number, number], 43 + b: [number, number, number, number], 44 + ): boolean { 45 + return !(a[2] < b[0] || a[0] > b[2] || a[3] < b[1] || a[1] > b[3]) 46 + } 47 + 23 48 type TileManifestEntry = { 24 49 id: string 25 50 filename?: string ··· 51 76 #tileManifest: TileManifestEntry[] = [] 52 77 #availableTile: TileManifestEntry | null = null 53 78 #confirmedCached = new Set<string>() 79 + #activeRegionalSources = new Set<string>() 54 80 #lastBookmarkValues: ReadonlyMap<string, unknown> | undefined = undefined 55 81 #lastCollectionValues: ReadonlyMap<string, unknown> | undefined = undefined 56 82 #currentZoom = 0 ··· 79 105 #onMoveEnd = debounce(() => { 80 106 this.#saveLastView() 81 107 this.#checkAvailableTile() 108 + this.#syncRegionalTilesForViewport() 82 109 this.#currentZoom = this.#map?.getZoom() ?? 0 83 110 this.requestUpdate() 84 111 }, 150) ··· 94 121 } 95 122 96 123 #onPMTilesUpdated = async () => { 97 - await this.#loadCachedDetailTiles(this.#tileManifest) 98 - this.#ensureBookmarkLayersOnTop() 124 + await this.#syncRegionalTilesForViewport() 99 125 } 100 126 101 127 override firstUpdated(): void { ··· 149 175 this.#map.on('load', async () => { 150 176 this.#currentZoom = this.#map!.getZoom() 151 177 await this.#loadWorldTiles() 152 - await this.#loadCachedDetailTiles() 178 + await this.#syncRegionalTilesForViewport() 153 179 this.#renderBookmarkMarkers() 154 180 this.#ensureBookmarkLayersOnTop() 155 181 const target = getMapNav() ··· 615 641 `/static/tiles/${this.#availableTile.filename}`, 616 642 this.#availableTile.filename, 617 643 ) 618 - await this.#loadCachedDetailTiles(manifest) 619 - this.#ensureBookmarkLayersOnTop() 644 + await this.#syncRegionalTilesForViewport() 620 645 this.#availableTile = null 621 646 this.requestUpdate() 622 647 } catch (err) { ··· 627 652 (t) => t.group === `${parentGroup}/${parentId}` && t.filename, 628 653 ) 629 654 630 - let failed = false 631 655 for (const tile of tilesInGroup) { 632 656 if (!tile.filename) continue 633 657 try { ··· 637 661 ) 638 662 } catch (err) { 639 663 console.error(`Download failed for ${tile.label}:`, err) 640 - failed = true 641 664 } 642 665 } 643 666 644 - if (!failed || failed) { 645 - await this.#loadCachedDetailTiles(manifest) 646 - this.#ensureBookmarkLayersOnTop() 647 - this.#availableTile = null 648 - this.requestUpdate() 649 - } 667 + await this.#syncRegionalTilesForViewport() 668 + this.#availableTile = null 669 + this.requestUpdate() 650 670 } 651 671 } 652 672 ··· 766 786 } 767 787 } 768 788 769 - async #loadCachedDetailTiles( 770 - manifest?: TileManifestEntry[], 771 - ): Promise<void> { 772 - if (!this.#map) return 773 - const tiles = manifest?.length ? manifest : await fetchTileManifest() 774 - // Insert regional layers below bookmark layers so bookmarks always 775 - // render on top regardless of when this is called. 789 + async #addRegionalSource(tile: TileManifestEntry): Promise<void> { 790 + if (!this.#map || !tile.filename) return 791 + if (this.#map.getSource(tile.id)) return 792 + const pmtiles = await getCachedPMTiles(tile.filename) 793 + if (!pmtiles) return 794 + if (!registeredSources.has(tile.id)) { 795 + protocol.add(pmtiles) 796 + registeredSources.add(tile.id) 797 + } 776 798 const beforeId = this.#map.getLayer('bookmarks-clusters') 777 799 ? 'bookmarks-clusters' 778 800 : undefined 779 - for (const tile of tiles) { 780 - if (this.#map.getSource(tile.id)) continue 781 - if (!tile.filename) continue 782 - const pmtiles = await getCachedPMTiles(tile.filename) 783 - if (!pmtiles) continue 784 - if (!registeredSources.has(tile.id)) { 785 - protocol.add(pmtiles) 786 - registeredSources.add(tile.id) 787 - } 788 - this.#map.addSource(tile.id, { 789 - type: 'vector', 790 - url: `pmtiles://${tile.filename}`, 791 - }) 792 - layers(tile.id).forEach((layer) => 793 - this.#map!.addLayer(layer as maplibregl.AddLayerObject, beforeId) 801 + this.#map.addSource(tile.id, { 802 + type: 'vector', 803 + url: `pmtiles://${tile.filename}`, 804 + }) 805 + layers(tile.id).forEach((layer) => 806 + this.#map!.addLayer(layer as maplibregl.AddLayerObject, beforeId) 807 + ) 808 + this.#activeRegionalSources.add(tile.id) 809 + } 810 + 811 + #removeRegionalSource(id: string): void { 812 + if (!this.#map) return 813 + for (const layer of layers(id)) { 814 + if (this.#map.getLayer(layer.id)) this.#map.removeLayer(layer.id) 815 + } 816 + if (this.#map.getSource(id)) this.#map.removeSource(id) 817 + this.#activeRegionalSources.delete(id) 818 + } 819 + 820 + async #syncRegionalTilesForViewport(): Promise<void> { 821 + if (!this.#map) return 822 + const tiles = this.#tileManifest.length 823 + ? this.#tileManifest 824 + : await fetchTileManifest() 825 + this.#tileManifest = tiles 826 + 827 + const desired = new Set<string>() 828 + if (this.#map.getZoom() >= MIN_REGIONAL_ZOOM) { 829 + const buffered = expandBounds(this.#map.getBounds(), VIEWPORT_BUFFER) 830 + const candidates = tiles.filter((t) => 831 + t.filename && t.bounds && boundsIntersect(t.bounds, buffered) 832 + ) 833 + const cached = await Promise.all( 834 + candidates.map((t) => isPMTilesCached(t.filename!)), 794 835 ) 836 + candidates.forEach((t, i) => { 837 + if (cached[i]) desired.add(t.id) 838 + }) 795 839 } 840 + 841 + for (const id of [...this.#activeRegionalSources]) { 842 + if (!desired.has(id)) this.#removeRegionalSource(id) 843 + } 844 + const tileById = new Map(tiles.map((t) => [t.id, t])) 845 + for (const id of desired) { 846 + if (this.#activeRegionalSources.has(id)) continue 847 + const tile = tileById.get(id) 848 + if (tile) await this.#addRegionalSource(tile) 849 + } 850 + 851 + this.#ensureBookmarkLayersOnTop() 796 852 } 797 853 } 798 854