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: update initial load to zoom to user

+116 -74
+6 -1
README.md
··· 1 - # maps.bpev.me 1 + <h1> 2 + MapsApp 3 + <a href="https://bpev.me/apps"> 4 + <img src="https://static.bpev.me/icons/made-by-ben.svg" /> 5 + </a> 6 + </h1> 2 7 3 8 MapsApp is an offline-capable geomap, meant for storing location bookmarks. 4 9
+8 -8
deno.json
··· 1 1 { 2 - "version": "0.4.2", 2 + "version": "0.4.3", 3 3 "workspace": ["./data"], 4 4 "tasks": { 5 5 "data": "deno run -A ./data/cli/main.ts", ··· 22 22 "imports": { 23 23 "@civility/blobs": "jsr:@civility/blobs@^1.0.0-beta.4", 24 24 "@civility/blobs/idb": "jsr:@civility/blobs@^1.0.0-beta.4/idb", 25 - "@civility/store": "jsr:@civility/store@^1.0.0-beta.8", 26 - "@civility/store/idb": "jsr:@civility/store@^1.0.0-beta.8/idb", 27 - "@civility/sync": "jsr:@civility/sync@^1.0.0-beta.10", 28 - "@civility/ui": "jsr:@civility/ui@^1.0.0-beta.3", 29 - "@civility/workers": "jsr:@civility/workers@^0.2.5", 30 - "@std/async": "jsr:@std/async@^1.2.0", 25 + "@civility/store": "jsr:@civility/store@^1.0.0-beta.10", 26 + "@civility/store/idb": "jsr:@civility/store@^1.0.0-beta.10/idb", 27 + "@civility/sync": "jsr:@civility/sync@^1.0.0-beta.13", 28 + "@civility/ui": "jsr:@civility/ui@^1.0.0-beta.5", 29 + "@civility/workers": "jsr:@civility/workers@^0.2.7", 30 + "@std/async": "jsr:@std/async@^1.3.0", 31 31 "@zod/zod": "jsr:@zod/zod@^4.3.6", 32 32 "fflate": "npm:fflate@^0.8.2", 33 33 "lit": "npm:lit@^3.3.2", 34 - "maplibre-gl": "npm:maplibre-gl@^5.23.0", 34 + "maplibre-gl": "npm:maplibre-gl@^5.24.0", 35 35 "pmtiles": "npm:pmtiles@^4.4.1", 36 36 "pmtiles-offline": "npm:pmtiles-offline@^1.0.0" 37 37 }
+62 -55
deno.lock
··· 2 2 "version": "5", 3 3 "specifiers": { 4 4 "jsr:@civility/blobs@^1.0.0-beta.4": "1.0.0-beta.4", 5 - "jsr:@civility/store@^1.0.0-beta.8": "1.0.0-beta.8", 6 - "jsr:@civility/sync@^1.0.0-beta.10": "1.0.0-beta.10", 7 - "jsr:@civility/ui@^1.0.0-beta.3": "1.0.0-beta.3", 8 - "jsr:@civility/workers@~0.2.5": "0.2.5", 9 - "jsr:@cliffy/command@1": "1.0.0", 10 - "jsr:@cliffy/flags@1.0.0": "1.0.0", 11 - "jsr:@cliffy/internal@1.0.0": "1.0.0", 12 - "jsr:@cliffy/table@1.0.0": "1.0.0", 5 + "jsr:@civility/errors@^1.0.0-beta.1": "1.0.0-beta.1", 6 + "jsr:@civility/store@^1.0.0-beta.10": "1.0.0-beta.10", 7 + "jsr:@civility/sync@^1.0.0-beta.13": "1.0.0-beta.13", 8 + "jsr:@civility/ui@^1.0.0-beta.5": "1.0.0-beta.5", 9 + "jsr:@civility/workers@~0.2.7": "0.2.7", 10 + "jsr:@cliffy/command@1": "1.0.1", 11 + "jsr:@cliffy/flags@1.0.1": "1.0.1", 12 + "jsr:@cliffy/internal@1.0.1": "1.0.1", 13 + "jsr:@cliffy/table@1.0.1": "1.0.1", 13 14 "jsr:@paulmillr/qr@~0.5.5": "0.5.5", 14 - "jsr:@std/async@^1.2.0": "1.2.0", 15 - "jsr:@std/fmt@^1.0.9": "1.0.9", 15 + "jsr:@std/async@^1.3.0": "1.3.0", 16 + "jsr:@std/data-structures@^1.0.11": "1.0.11", 17 + "jsr:@std/fmt@^1.0.9": "1.0.10", 16 18 "jsr:@std/fs@1": "1.0.23", 17 19 "jsr:@std/fs@^1.0.23": "1.0.23", 18 - "jsr:@std/html@^1.0.5": "1.0.5", 19 - "jsr:@std/internal@^1.0.12": "1.0.12", 20 + "jsr:@std/html@^1.0.5": "1.0.6", 21 + "jsr:@std/internal@^1.0.12": "1.0.13", 20 22 "jsr:@std/path@^1.1.4": "1.1.4", 21 23 "jsr:@std/semver@^1.0.8": "1.0.8", 22 - "jsr:@std/text@^1.0.17": "1.0.17", 24 + "jsr:@std/text@^1.0.17": "1.0.18", 23 25 "jsr:@std/ulid@1": "1.0.0", 24 26 "jsr:@zod/zod@^4.3.6": "4.3.6", 25 27 "npm:cheerio@^1.2.0": "1.2.0", 26 28 "npm:fast-json-patch@^3.1.1": "3.1.1", 27 29 "npm:fflate@~0.8.2": "0.8.2", 28 30 "npm:lit@^3.3.2": "3.3.2", 29 - "npm:maplibre-gl@^5.23.0": "5.23.0", 30 - "npm:pmtiles-offline@1": "1.0.0", 31 + "npm:maplibre-gl@^5.24.0": "5.24.0", 31 32 "npm:pmtiles@^4.4.1": "4.4.1" 32 33 }, 33 34 "jsr": { 34 35 "@civility/blobs@1.0.0-beta.4": { 35 36 "integrity": "6806eb2a5b02e9e611385107b539abe0b2fe8e17066cfc42eaf467e301a6afa0" 36 37 }, 37 - "@civility/store@1.0.0-beta.8": { 38 - "integrity": "97d24ab2100fd2dbc5665e45abc1d0de68c81f5b717ddc95313c6a1502eb0546", 38 + "@civility/errors@1.0.0-beta.1": { 39 + "integrity": "2b28c161162aa855498ba7a7c9fe95b43cc149cc8bfb1b70bfa2f895c1cc186d" 40 + }, 41 + "@civility/store@1.0.0-beta.10": { 42 + "integrity": "9fe3ec0c4dbfe15e149462e8208e425644f686dd20a9caf3e64a1833e519a389", 39 43 "dependencies": [ 40 44 "jsr:@std/fs@^1.0.23", 41 45 "jsr:@std/path", ··· 44 48 "npm:fast-json-patch" 45 49 ] 46 50 }, 47 - "@civility/sync@1.0.0-beta.10": { 48 - "integrity": "15345da12c2b3d7f83e31626350dd066561a5cac86cb317f094c8ede73872183", 51 + "@civility/sync@1.0.0-beta.13": { 52 + "integrity": "ca1b957583be279176fa3c32ef672bdeac418fe5980178094c9aa651a6405f7b", 49 53 "dependencies": [ 50 54 "jsr:@civility/blobs", 55 + "jsr:@civility/errors", 51 56 "jsr:@civility/store", 52 57 "jsr:@paulmillr/qr" 53 58 ] 54 59 }, 55 - "@civility/ui@1.0.0-beta.3": { 56 - "integrity": "4c35d660ff511ef45d824ac1941928b1fc099930a9764f81ce7cb9c416df9588", 60 + "@civility/ui@1.0.0-beta.5": { 61 + "integrity": "fefa2b541adcf9bbda3fc0dfea07dcab99612497d647856ba76d857c6d98a8b7", 57 62 "dependencies": [ 58 63 "jsr:@std/html", 59 64 "npm:lit" ··· 62 67 "@civility/workers@0.2.4": { 63 68 "integrity": "38fafb96bc15a988e7723bc9b021394bdfef842c8e8372b960ec2476e5c74b43" 64 69 }, 65 - "@civility/workers@0.2.5": { 66 - "integrity": "5a27340c55972cc71042d4b3ce9c6a8d508e31a77fe6133f94ccdb0d48db0e40" 70 + "@civility/workers@0.2.7": { 71 + "integrity": "c2485c3d3dc867ff0f40d846e40a6fcc3992a11d27309ae5dd31851f0bc0e4b7" 67 72 }, 68 - "@cliffy/command@1.0.0": { 69 - "integrity": "c52a241ea68857fcdaff4f3173eb404f8017d7bc35553b6f533c592b89dde7d2", 73 + "@cliffy/command@1.0.1": { 74 + "integrity": "0172d9c7d8aeb26b8f4f44ffa833247e4fbab707e258ee271e10a2a2f87b531d", 70 75 "dependencies": [ 71 76 "jsr:@cliffy/flags", 72 77 "jsr:@cliffy/internal", ··· 76 81 "jsr:@std/text" 77 82 ] 78 83 }, 79 - "@cliffy/flags@1.0.0": { 80 - "integrity": "8b57698adc644da8f90422d58976362d41a4ebca39c312ca1c101585d0148feb", 84 + "@cliffy/flags@1.0.1": { 85 + "integrity": "468dcf65acb33b51ecb2e232c30603209c528b1d1bd9e065921b57fcb342cb01", 81 86 "dependencies": [ 82 87 "jsr:@cliffy/internal", 83 88 "jsr:@std/text" 84 89 ] 85 90 }, 86 - "@cliffy/internal@1.0.0": { 87 - "integrity": "1e17ccbcd5420093c0a93e5b3827bbdc9abac5195bacf187edc44665e54bdde6" 91 + "@cliffy/internal@1.0.1": { 92 + "integrity": "9e2bba59ad559b790f09c57219c727a69f0179ebabc07f1bf9db25232b606760" 88 93 }, 89 - "@cliffy/table@1.0.0": { 90 - "integrity": "3fdaa9e1ef1ea62022108adabd826932bdea8dd05497079896febcd41322907f", 94 + "@cliffy/table@1.0.1": { 95 + "integrity": "2b1baa3ef9e16ecb7f3fdcf1deb9f5fa5ffa308ea0e68478224a4c2d44d58166", 91 96 "dependencies": [ 92 97 "jsr:@std/fmt" 93 98 ] ··· 95 100 "@paulmillr/qr@0.5.5": { 96 101 "integrity": "2f8ff22c8d2194f2147eac1b3093f5e85f648c0a8005d5635a617fb72bf5ae38" 97 102 }, 98 - "@std/async@1.2.0": { 99 - "integrity": "c059c6f6d95ca7cc012ae8e8d7164d1697113d54b0b679e4372b354b11c2dee5" 103 + "@std/async@1.3.0": { 104 + "integrity": "80485538a4f7baaa46bfe2246168069e02ed142b9f9079cd164f43bb060ad9e9", 105 + "dependencies": [ 106 + "jsr:@std/data-structures" 107 + ] 108 + }, 109 + "@std/data-structures@1.0.11": { 110 + "integrity": "53b98ed7efa61f107dfc14244bd2ec5557f7f7ee0bbaef6d449d7937facacb89" 100 111 }, 101 - "@std/fmt@1.0.9": { 102 - "integrity": "2487343e8899fb2be5d0e3d35013e54477ada198854e52dd05ed0422eddcabe0" 112 + "@std/fmt@1.0.10": { 113 + "integrity": "90dfba288802ac6de82fb31d0917eb9e4450b9925b954d5e51fc29ac07419db5" 103 114 }, 104 115 "@std/fs@1.0.23": { 105 116 "integrity": "3ecbae4ce4fee03b180fa710caff36bb5adb66631c46a6460aaad49515565a37", ··· 108 119 "jsr:@std/path" 109 120 ] 110 121 }, 111 - "@std/html@1.0.5": { 112 - "integrity": "4e2d693f474cae8c16a920fa5e15a3b72267b94b84667f11a50c6dd1cb18d35e" 122 + "@std/html@1.0.6": { 123 + "integrity": "eaf759c8141e0733ca30eb49e4c08d8e6ca442b85c4d51f9894a56f502993e08" 113 124 }, 114 - "@std/internal@1.0.12": { 115 - "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" 125 + "@std/internal@1.0.13": { 126 + "integrity": "2f9546691d4ac2d32859c82dff284aaeac980ddeca38430d07941e7e288725c0" 116 127 }, 117 128 "@std/path@1.1.4": { 118 129 "integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5", ··· 123 134 "@std/semver@1.0.8": { 124 135 "integrity": "dc830e8b8b6a380c895d53fbfd1258dc253704ca57bbe1629ac65fd7830179b7" 125 136 }, 126 - "@std/text@1.0.17": { 127 - "integrity": "4b2c4ef67ae5b6c1dfd447c81c83a43718f52e3c7e748d8b33f694aba9895f95" 137 + "@std/text@1.0.18": { 138 + "integrity": "d199e516f80599813c64fd4aee5b8f26f6f7d1e1434c88fd153aeea6fea6a9b9" 128 139 }, 129 140 "@std/ulid@1.0.0": { 130 141 "integrity": "d41c3d27a907714413649fee864b7cde8d42ee68437d22b79d5de4f81d808780" ··· 188 199 ], 189 200 "bin": true 190 201 }, 191 - "@maplibre/mlt@1.1.8": { 192 - "integrity": "sha512-8vtfYGidr1rNkv5IwIoU2lfe3Oy+Wa8HluzQYcQi9cveU9K3pweAal/poQj4GJ0K/EW4bTQp2wVAs09g2yDRZg==", 202 + "@maplibre/mlt@1.1.9": { 203 + "integrity": "sha512-g/tD8EYJB97udq33ipuJ9a4Q7fcbZnTEnUrgnEc/tLMmEL+zaCbR+X5fkDBO2dgpaAMsLH179qE3UXg2N0Nc/g==", 193 204 "dependencies": [ 194 205 "@mapbox/point-geometry" 195 206 ] ··· 357 368 "lit-html" 358 369 ] 359 370 }, 360 - "maplibre-gl@5.23.0": { 361 - "integrity": "sha512-aou8YBNFS8uVtDWFWt0W/6oorfl18wt+oIA8fnXk1kivjkbtXi9gGrQvflTpwrR3hG13aWdIdbYWeN0NFMV7ag==", 371 + "maplibre-gl@5.24.0": { 372 + "integrity": "sha512-ALyFxgtd5R+65UqZ/++lOqwWcC0SNho9c27fYSyLmG7AfnAul2o46F05aDJGPbFU57wos9dgcIySHs0Xe6ia3A==", 362 373 "dependencies": [ 363 374 "@mapbox/jsonlint-lines-primitives", 364 375 "@mapbox/point-geometry", ··· 419 430 ], 420 431 "bin": true 421 432 }, 422 - "pmtiles-offline@1.0.0": { 423 - "integrity": "sha512-bagqJVRs5VDb6LkpNMYqk5/18wnV9LQcjLFYQTU4Y8c2u2QONIuV7QVlXZ+0JKvb/xdrEzrR9jgQv0qyfoN0eg==" 424 - }, 425 433 "pmtiles@4.4.1": { 426 434 "integrity": "sha512-5oTeQc/yX/ft1evbpIlnoCZugQuug/iYIAj/ZTqIqzdGek4uZEho99En890EE6NOSI3JTI3IG8R7r8+SltphxA==", 427 435 "dependencies": [ ··· 475 483 "workspace": { 476 484 "dependencies": [ 477 485 "jsr:@civility/blobs@^1.0.0-beta.4", 478 - "jsr:@civility/store@^1.0.0-beta.8", 479 - "jsr:@civility/sync@^1.0.0-beta.10", 480 - "jsr:@civility/ui@^1.0.0-beta.3", 481 - "jsr:@civility/workers@~0.2.5", 482 - "jsr:@std/async@^1.2.0", 486 + "jsr:@civility/store@^1.0.0-beta.10", 487 + "jsr:@civility/sync@^1.0.0-beta.13", 488 + "jsr:@civility/ui@^1.0.0-beta.5", 489 + "jsr:@civility/workers@~0.2.7", 490 + "jsr:@std/async@^1.3.0", 483 491 "jsr:@zod/zod@^4.3.6", 484 492 "npm:fflate@~0.8.2", 485 493 "npm:lit@^3.3.2", 486 - "npm:maplibre-gl@^5.23.0", 487 - "npm:pmtiles-offline@1", 494 + "npm:maplibre-gl@^5.24.0", 488 495 "npm:pmtiles@^4.4.1" 489 496 ], 490 497 "members": {
+7 -7
www/components/m-map.ts
··· 39 39 #map: maplibregl.Map | null = null 40 40 #marker: maplibregl.Marker | null = null 41 41 #bookmarkPopup: maplibregl.Popup | null = null 42 + #geolocate: maplibregl.GeolocateControl | null = null 42 43 #longPressTimer: ReturnType<typeof setTimeout> | null = null 43 44 #tileManifest: TileManifestEntry[] = [] 44 45 #availableTile: TileManifestEntry | null = null ··· 113 114 }, 114 115 }) 115 116 116 - this.#map.addControl( 117 - new maplibregl.GeolocateControl({ 118 - positionOptions: { enableHighAccuracy: true }, 119 - trackUserLocation: true, 120 - }), 121 - 'top-right', 122 - ) 117 + this.#geolocate = new maplibregl.GeolocateControl({ 118 + positionOptions: { enableHighAccuracy: true }, 119 + trackUserLocation: true, 120 + }) 121 + this.#map.addControl(this.#geolocate, 'top-right') 123 122 124 123 this.#map.on('moveend', this.#onMoveEnd) 125 124 ··· 143 142 this.#renderBookmarkMarkers() 144 143 ensureBookmarkLayersOnTop(this.#map!) 145 144 this.#applyPendingNav() 145 + if (app.trackOnInitialLoad) this.#geolocate?.trigger() 146 146 }) 147 147 } 148 148
+1
www/components/m-welcome.ts
··· 49 49 } 50 50 this.#locationStatus = 'requesting' 51 51 this.requestUpdate() 52 + app.setTrackOnInitialLoad(true) 52 53 53 54 navigator.geolocation.getCurrentPosition( 54 55 async (pos) => {
+1
www/index.html
··· 9 9 <meta name="mobile-web-app-capable" content="yes"> 10 10 <meta name="apple-mobile-web-app-capable" content="yes"> 11 11 <meta name="description" content="An offline-capable world map"> 12 + <meta name="theme-color" content="#327759"> 12 13 13 14 <link rel="manifest" href="manifest.json" /> 14 15 <link rel="icon" type="image/x-icon" href="/dist/icons/icon.ico" />
+10
www/models/app.ts
··· 15 15 type Preferences = { 16 16 geocodingBookmarksEnabled: boolean 17 17 onlineSearchEnabled: boolean 18 + trackOnInitialLoad: boolean 18 19 lastView: LastView | null 19 20 hasSeenWelcome: boolean 20 21 } ··· 22 23 const defaultPreferences: Preferences = { 23 24 geocodingBookmarksEnabled: true, 24 25 onlineSearchEnabled: true, 26 + trackOnInitialLoad: false, 25 27 lastView: null, 26 28 hasSeenWelcome: false, 27 29 } ··· 253 255 254 256 async setOnlineSearchEnabled(value: boolean): Promise<void> { 255 257 await preferencesDoc.update({ onlineSearchEnabled: value }) 258 + } 259 + 260 + get trackOnInitialLoad(): boolean { 261 + return preferencesDoc.value?.trackOnInitialLoad ?? false 262 + } 263 + 264 + async setTrackOnInitialLoad(value: boolean): Promise<void> { 265 + await preferencesDoc.update({ trackOnInitialLoad: value }) 256 266 } 257 267 258 268 get lastView(): LastView | null {
+17
www/routes/settings.ts
··· 77 77 @change="${this.#handleGeocodingToggle}" 78 78 > 79 79 </label> 80 + <label class="settings-toggle-label"> 81 + <div> 82 + <span>Find my location on load</span> 83 + <div class="settings-nav-link-meta"> 84 + Automatically start tracking your location when the map opens. 85 + </div> 86 + </div> 87 + <input 88 + type="checkbox" 89 + .checked="${app.trackOnInitialLoad}" 90 + @change="${this.#handleTrackOnInitialLoadToggle}" 91 + > 92 + </label> 80 93 </section> 81 94 82 95 <section> ··· 124 137 125 138 #handleGeocodingToggle(e: Event): void { 126 139 app.setGeocodingBookmarksEnabled((e.target as HTMLInputElement).checked) 140 + } 141 + 142 + #handleTrackOnInitialLoadToggle(e: Event): void { 143 + app.setTrackOnInitialLoad((e.target as HTMLInputElement).checked) 127 144 } 128 145 } 129 146
+4 -3
www/utils/fs.ts
··· 3 3 4 4 const DB_NAME = 'bpev-maps-tiles' 5 5 const STORE_NAME = 'tiles' 6 + const options = { logLevel: 'warn' } 6 7 7 8 let _db: IDBDatabase | null = null 8 9 ··· 16 17 filename: string, 17 18 ): Promise<PMTiles> { 18 19 const db = await getDb() 19 - const source = new IndexedDBSource(db, filename, STORE_NAME) 20 + const source = new IndexedDBSource(db, filename, STORE_NAME, options) 20 21 21 22 const exists = await source.exists() 22 23 if (!exists) { ··· 35 36 36 37 export async function isPMTilesCached(filename: string): Promise<boolean> { 37 38 const db = await getDb() 38 - const source = new IndexedDBSource(db, filename, STORE_NAME) 39 + const source = new IndexedDBSource(db, filename, STORE_NAME, options) 39 40 return await source.exists() 40 41 } 41 42 ··· 43 44 filename: string, 44 45 ): Promise<PMTiles | null> { 45 46 const db = await getDb() 46 - const source = new IndexedDBSource(db, filename, STORE_NAME) 47 + const source = new IndexedDBSource(db, filename, STORE_NAME, options) 47 48 if (!(await source.exists())) return null 48 49 return new PMTiles(source) 49 50 }