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

Configure Feed

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

fix: issue where bookmarks don't navigate

+121 -90
+46 -32
www/components/m-map.ts
··· 7 7 getCachedPMTiles, 8 8 isPMTilesCached, 9 9 } from '../utils/fs.ts' 10 - import { getMapNav, setMapNav } from '../utils/nav.ts' 10 + import { getMapNav, MAP_NAV_EVENT, setMapNav } from '../utils/nav.ts' 11 11 import layers from '../utils/layers.ts' 12 12 import { WORLD_LAYERS } from '../utils/layers.ts' 13 13 import app from '../models/app.ts' ··· 56 56 super.connectedCallback() 57 57 app.addEventListener(this.#onAppUpdate) 58 58 globalThis.addEventListener('pmtiles-updated', this.#onPMTilesUpdated) 59 + globalThis.addEventListener(MAP_NAV_EVENT, this.#onMapNav) 59 60 } 60 61 61 - override updated(changedProperties: Map<string, unknown>): void { 62 - if (changedProperties.has('hidden') && !this.hidden && this.#map) { 63 - this.#map.resize() 64 - } 62 + override updated(): void { 65 63 if (globalThis.__DEV__) { 66 64 const zoom = document.querySelector<HTMLElement>('.zoom-display') 67 65 if (zoom) zoom.hidden = false 68 - console.log(zoom) 69 66 } 70 67 } 71 68 ··· 145 142 await this.#syncRegionalTilesForViewport() 146 143 this.#renderBookmarkMarkers() 147 144 ensureBookmarkLayersOnTop(this.#map!) 148 - const target = getMapNav() 149 - if (target) { 150 - setMapNav(null) 151 - this.#map!.flyTo({ 152 - center: [target.lng, target.lat], 153 - zoom: target.zoom, 154 - }) 155 - if (target.marker) { 156 - this.#marker?.remove() 157 - this.#marker = new maplibregl.Marker({ color: '#16a34a' }) 158 - .setLngLat([target.lng, target.lat]) 159 - .setPopup( 160 - createBookmarkAddPopup({ 161 - lat: target.lat, 162 - lng: target.lng, 163 - zoom: target.zoom, 164 - initialName: target.name ?? 'Unknown location', 165 - address: target.address, 166 - }), 167 - ) 168 - .addTo(this.#map!) 169 - this.#marker.togglePopup() 170 - } else if (target.bookmarkId) { 171 - this.#showBookmarkPopup(target.bookmarkId) 172 - } 173 - } 145 + this.#applyPendingNav() 146 + }) 147 + } 148 + 149 + #onMapNav = () => { 150 + if (!this.#map?.loaded()) return 151 + // Nav events often arrive while m-map is still hidden (the triggering 152 + // route hasn't finished the hashchange → unhide cycle yet). Defer a 153 + // frame so layout settles, then resize so flyTo uses real dimensions. 154 + requestAnimationFrame(() => { 155 + if (!this.#map) return 156 + this.#map.resize() 157 + this.#applyPendingNav() 158 + }) 159 + } 160 + 161 + #applyPendingNav(): void { 162 + if (!this.#map) return 163 + const target = getMapNav() 164 + if (!target) return 165 + setMapNav(null) 166 + this.#map.flyTo({ 167 + center: [target.lng, target.lat], 168 + zoom: target.zoom, 174 169 }) 170 + if (target.marker) { 171 + this.#marker?.remove() 172 + this.#marker = new maplibregl.Marker({ color: '#16a34a' }) 173 + .setLngLat([target.lng, target.lat]) 174 + .setPopup( 175 + createBookmarkAddPopup({ 176 + lat: target.lat, 177 + lng: target.lng, 178 + zoom: target.zoom, 179 + initialName: target.name ?? 'Unknown location', 180 + address: target.address, 181 + }), 182 + ) 183 + .addTo(this.#map) 184 + this.#marker.togglePopup() 185 + } else if (target.bookmarkId) { 186 + this.#showBookmarkPopup(target.bookmarkId) 187 + } 175 188 } 176 189 177 190 #saveLastView() { ··· 415 428 super.disconnectedCallback() 416 429 app.removeEventListener(this.#onAppUpdate) 417 430 globalThis.removeEventListener('pmtiles-updated', this.#onPMTilesUpdated) 431 + globalThis.removeEventListener(MAP_NAV_EVENT, this.#onMapNav) 418 432 this.#onMoveEnd.clear() 419 433 this.#clearLongPress() 420 434 this.#bookmarkPopup?.remove()
+59 -48
www/index.html
··· 1 1 <!DOCTYPE html> 2 2 <html lang="en"> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <meta 6 + name="viewport" 7 + content="width=device-width, initial-scale=1, viewport-fit=cover" 8 + > 9 + <meta name="mobile-web-app-capable" content="yes"> 10 + <meta name="apple-mobile-web-app-capable" content="yes"> 11 + <meta name="description" content="An offline-capable world map"> 3 12 4 - <head> 5 - <meta charset="utf-8" /> 6 - <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> 7 - <meta name="mobile-web-app-capable" content="yes"> 8 - <meta name="apple-mobile-web-app-capable" content="yes"> 9 - <meta name="description" content="An offline-capable world map"> 13 + <link rel="manifest" href="manifest.json" /> 14 + <link rel="icon" type="image/x-icon" href="/dist/icons/icon.ico" /> 15 + <link rel="apple-touch-icon" href="/dist/icons/icon.png"> 10 16 11 - <link rel="manifest" href="manifest.json" /> 12 - <link rel="icon" type="image/x-icon" href="/dist/icons/icon.ico" /> 13 - <link rel="apple-touch-icon" href="/dist/icons/icon.png"> 17 + <link rel="canonical" href="https://maps.bpev.me" /> 18 + <title>MapsApp</title> 14 19 15 - <link rel="canonical" href="https://maps.bpev.me" /> 16 - <title>MapsApp</title> 20 + <link 21 + rel="stylesheet" 22 + type="text/css" 23 + href="/static/styles/maplibre-gl.css" 24 + > 25 + <link 26 + rel="stylesheet" 27 + type="text/css" 28 + href="https://bpev.me/civility.min.css" 29 + > 30 + <link rel="stylesheet" type="text/css" href="/static/styles/theme.css"> 17 31 18 - <link rel="stylesheet" type="text/css" href="/static/styles/maplibre-gl.css"> 19 - <link rel="stylesheet" type="text/css" href="https://bpev.me/civility.min.css"> 20 - <link rel="stylesheet" type="text/css" href="/static/styles/theme.css"> 32 + <script src="/dist/index.js" type="module"></script> 33 + </head> 21 34 22 - <script src="/dist/index.js" type="module"></script> 23 - </head> 35 + <body> 36 + <a href="#main" class="skip-to-main">Skip to main content</a> 24 37 25 - <body> 26 - <a href="#main" class="skip-to-main">Skip to main content</a> 38 + <header></header> 27 39 28 - <header></header> 40 + <main id="main"> 41 + <div 42 + style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)" 43 + > 44 + <ui-spinner size="lg"></ui-spinner> 45 + </div> 46 + </main> 29 47 30 - <main id="main"> 31 - <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)"> 32 - <ui-spinner size="lg"></ui-spinner> 33 - </div> 34 - </main> 48 + <m-map></m-map> 35 49 36 - <m-map></m-map> 37 - 38 - <footer fixed> 39 - <ui-bottom-bar role="navigation" aria-label="Bottom navigation"> 40 - <a href="/" data-route aria-current="page"> 41 - <img src="/static/icons/home.svg" alt="" aria-hidden="true"> 42 - <span>Map</span> 43 - </a> 44 - <a href="/search" data-route> 45 - <img src="/static/icons/navigation.svg" alt="" aria-hidden="true"> 46 - <span>Search</span> 47 - </a> 48 - <a href="/bookmarks" data-route> 49 - <img src="/static/icons/bookmark.svg" alt="" aria-hidden="true"> 50 - <span>Bookmarks</span> 51 - </a> 52 - <a href="/settings" data-route> 53 - <img src="/static/icons/tool.svg" alt="" aria-hidden="true"> 54 - <span>Settings</span> 55 - </a> 56 - </ui-bottom-bar> 57 - </footer> 58 - </body> 59 - 50 + <footer fixed> 51 + <ui-bottom-bar role="navigation" aria-label="Bottom navigation"> 52 + <a href="/" data-route aria-current="page"> 53 + <img src="/static/icons/home.svg" alt="" aria-hidden="true"> 54 + <span>Map</span> 55 + </a> 56 + <a href="/search" data-route> 57 + <img src="/static/icons/navigation.svg" alt="" aria-hidden="true"> 58 + <span>Search</span> 59 + </a> 60 + <a href="/bookmarks" data-route> 61 + <img src="/static/icons/bookmark.svg" alt="" aria-hidden="true"> 62 + <span>Bookmarks</span> 63 + </a> 64 + <a href="/settings" data-route> 65 + <img src="/static/icons/tool.svg" alt="" aria-hidden="true"> 66 + <span>Settings</span> 67 + </a> 68 + </ui-bottom-bar> 69 + </footer> 70 + </body> 60 71 </html>
+13 -10
www/static/styles/theme.css
··· 59 59 overflow: hidden; 60 60 } 61 61 62 - body:has(r-home)>main { 62 + body:has(r-home) > main { 63 63 overflow: hidden; 64 64 } 65 65 ··· 96 96 z-index: 1; 97 97 } 98 98 99 + m-map .maplibregl-marker { 100 + z-index: 5; 101 + } 102 + 99 103 m-map .maplibregl-popup { 100 104 z-index: 100; 101 105 } 102 106 103 - m-map>.download-btn { 107 + m-map > .download-btn { 104 108 z-index: 10; 105 109 } 106 110 ··· 228 232 margin-bottom: var(--s3); 229 233 } 230 234 231 - r-settings section>p, 232 - r-settings-downloads section>p, 233 - r-settings-about section>p { 235 + r-settings section > p, 236 + r-settings-downloads section > p, 237 + r-settings-about section > p { 234 238 opacity: 0.6; 235 239 margin-bottom: var(--s3); 236 240 font-size: var(--f5); ··· 420 424 transform: none; 421 425 } 422 426 423 - .search-history-item>span { 427 + .search-history-item > span { 424 428 flex: 1; 425 429 min-width: 0; 426 430 overflow: hidden; ··· 428 432 white-space: nowrap; 429 433 } 430 434 431 - .search-history-item>img { 435 + .search-history-item > img { 432 436 flex-shrink: 0; 433 437 opacity: 0.35; 434 438 } ··· 743 747 margin: 0; 744 748 } 745 749 746 - .bm-import-group+.bm-import-group { 750 + .bm-import-group + .bm-import-group { 747 751 border-top: 1px solid currentColor; 748 752 } 749 753 ··· 764 768 gap: 1px; 765 769 } 766 770 767 - .bm-import-item+.bm-import-item { 771 + .bm-import-item + .bm-import-item { 768 772 border-top: 1px solid color-mix(in srgb, currentColor 15%, transparent); 769 773 } 770 774 ··· 781 785 } 782 786 783 787 @media (max-width: 768px) { 784 - 785 788 input, 786 789 textarea, 787 790 select {
+3
www/utils/nav.ts
··· 10 10 11 11 let pending: MapTarget | null = null 12 12 13 + export const MAP_NAV_EVENT = 'map-nav' 14 + 13 15 export function setMapNav(target: MapTarget | null): void { 14 16 pending = target 17 + if (target) globalThis.dispatchEvent(new CustomEvent(MAP_NAV_EVENT)) 15 18 } 16 19 17 20 export function getMapNav(): MapTarget | null {