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 artifacts destroy coastline

+159 -97
+1 -3
data/cli/shared/tilemaker/config.world.json
··· 29 29 "minzoom": 0, 30 30 "maxzoom": 9, 31 31 "source": "./data/cli/shared/tilemaker/coastline/water_polygons.shp", 32 - "filter_below": 9, 33 - "filter_area": 0.5, 34 - "simplify_below": 8, 32 + "simplify_below": 5, 35 33 "simplify_level": 0.0001, 36 34 "simplify_ratio": 2, 37 35 "simplify_algorithm": "visvalingam",
+68
data/cli/shared/tilemaker/config.world.json.tmp
··· 1 + { 2 + "layers": { 3 + "place": { 4 + "minzoom": 0, 5 + "maxzoom": 5 6 + }, 7 + "boundary": { 8 + "minzoom": 0, 9 + "maxzoom": 5, 10 + "simplify_below": 8, 11 + "simplify_level": 0.0003, 12 + "simplify_ratio": 2, 13 + "simplify_algorithm": "visvalingam" 14 + }, 15 + "waterway": { 16 + "minzoom": 8, 17 + "maxzoom": 5, 18 + "simplify_below": 9, 19 + "simplify_level": 0.0003, 20 + "simplify_ratio": 2 21 + }, 22 + "water": { 23 + "minzoom": 4, 24 + "maxzoom": 5, 25 + "simplify_below": 8, 26 + "simplify_level": 0.0003, 27 + "simplify_ratio": 2 28 + }, 29 + "ocean": { 30 + "minzoom": 0, 31 + "maxzoom": 5, 32 + "source": "./data/cli/shared/tilemaker/coastline/water_polygons.shp", 33 + "simplify_below": 5, 34 + "simplify_level": 0.0001, 35 + "simplify_ratio": 2, 36 + "simplify_algorithm": "visvalingam", 37 + "write_to": "water" 38 + }, 39 + "park": { 40 + "minzoom": 7, 41 + "maxzoom": 5 42 + }, 43 + "landcover": { 44 + "minzoom": 0, 45 + "maxzoom": 5, 46 + "simplify_below": 9, 47 + "simplify_level": 0.0003, 48 + "simplify_ratio": 2 49 + } 50 + }, 51 + "settings": { 52 + "minzoom": 0, 53 + "maxzoom": 5, 54 + "basezoom": 5, 55 + "include_ids": false, 56 + "combine_below": 5, 57 + "name": "World overview", 58 + "version": "1.0", 59 + "description": "World overview tiles from OSM planet", 60 + "compress": "gzip", 61 + "filemetadata": { 62 + "tilejson": "2.0.0", 63 + "scheme": "xyz", 64 + "type": "baselayer", 65 + "format": "pbf" 66 + } 67 + } 68 + }
+2 -2
docs/layers.md
··· 134 134 135 135 | # | Layer ID | Type | Source-layer | Notes | 136 136 | -- | ------------------ | ------ | ------------ | ------------------------------------------------------------------------------ | 137 - | 1 | `water` | fill | water | Oceans + inland water | 138 - | 2 | `landcover` | fill | landcover | **Merged** grass, wood, wetland, sand, rock, ice — class-based color + opacity | 137 + | 1 | `landcover` | fill | landcover | **Merged** grass, wood, wetland, sand, rock, ice — class-based color + opacity | 138 + | 2 | `water` | fill | water | Oceans + inland water — above landcover to mask coast-overlapping polygons | 139 139 | 3 | `park` | fill | park | National parks, nature reserves | 140 140 | 4 | `waterway` | line | waterway | z6+, named rivers | 141 141 | 5 | `boundary-state` | line | boundary | z3+, admin_level 3–6 |
+7 -3
www/components/m-map.ts
··· 682 682 683 683 return html` 684 684 <div id="map"></div> 685 - <div class="zoom-display">Zoom: ${this.#currentZoom.toFixed(1)}</div> 685 + <div class="zoom-display" ${globalThis.__DEV__ ? '' : 'hidden'}> 686 + Zoom: ${this.#currentZoom.toFixed(1)} 687 + </div> 686 688 ${tile 687 689 ? html` 688 690 <button ··· 729 731 730 732 async #loadWorldTiles(): Promise<void> { 731 733 if (!this.#map) return 732 - let worldFilename = 'world_z5.pmtiles' 734 + let worldFilename = 'world/world_z5.pmtiles' 733 735 if (!registeredSources.has('world')) { 734 736 const [z7, z6] = await Promise.all([ 735 737 getCachedPMTiles('world/world_z7.pmtiles'), ··· 738 740 739 741 if (z7) worldFilename = 'world/world_z7.pmtiles' 740 742 else if (z6) worldFilename = 'world/world_z6.pmtiles' 743 + 741 744 const pmtiles = z7 ?? z6 ?? await downloadAndSavePMTiles( 742 745 '/static/tiles/world/world_z5.pmtiles', 743 - 'world_z5.pmtiles', 746 + worldFilename, 744 747 ) 745 748 protocol.add(pmtiles) 746 749 registeredSources.add('world') ··· 751 754 url: `pmtiles://${worldFilename}`, 752 755 attribution: '© OpenStreetMap contributors', 753 756 }) 757 + 754 758 WORLD_LAYERS.forEach((layer) => 755 759 this.#map!.addLayer(layer as maplibregl.AddLayerObject) 756 760 )
+53 -64
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"> 12 3 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"> 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"> 16 10 17 - <link rel="canonical" href="https://maps.bpev.me" /> 18 - <title>MapsApp</title> 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"> 14 + 15 + <link rel="canonical" href="https://maps.bpev.me" /> 16 + <title>MapsApp</title> 17 + 18 + <link rel="stylesheet" type="text/css" href="/static/styles/maplibre-gl.css"> 19 + <link rel="stylesheet" type="text/css" href="/static/styles/civility.min.css"> 20 + <link rel="stylesheet" type="text/css" href="/static/styles/theme.css"> 19 21 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="/static/styles/civility.min.css" 29 - > 30 - <link rel="stylesheet" type="text/css" href="/static/styles/theme.css"> 22 + <script src="/dist/index.js" type="module"></script> 23 + </head> 31 24 32 - <script src="/dist/index.js" type="module"></script> 33 - </head> 25 + <body> 26 + <a href="#main" class="skip-to-main">Skip to main content</a> 34 27 35 - <body> 36 - <a href="#main" class="skip-to-main">Skip to main content</a> 28 + <header hidden> 29 + <a id="header-back" href="#!/" hidden aria-label="Back"> 30 + ← Back 31 + </a> 32 + <strong id="page-title">MapsApp</strong> 33 + </header> 37 34 38 - <header> 39 - <a id="header-back" href="#!/" hidden aria-label="Back"> 40 - ← Back 41 - </a> 42 - <strong id="page-title">MapsApp</strong> 43 - </header> 35 + <main id="main"> 36 + <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)"> 37 + <ui-spinner size="lg"></ui-spinner> 38 + </div> 39 + </main> 44 40 45 - <main id="main"> 46 - <div 47 - style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)" 48 - > 49 - <ui-spinner size="lg"></ui-spinner> 50 - </div> 51 - </main> 41 + <m-map></m-map> 52 42 53 - <m-map></m-map> 43 + <footer fixed> 44 + <ui-bottom-bar role="navigation" aria-label="Bottom navigation"> 45 + <a href="/" data-route aria-current="page"> 46 + <img src="/static/icons/home.svg" alt="" aria-hidden="true"> 47 + <span>Map</span> 48 + </a> 49 + <a href="/search" data-route> 50 + <img src="/static/icons/navigation.svg" alt="" aria-hidden="true"> 51 + <span>Search</span> 52 + </a> 53 + <a href="/bookmarks" data-route> 54 + <img src="/static/icons/bookmark.svg" alt="" aria-hidden="true"> 55 + <span>Bookmarks</span> 56 + </a> 57 + <a href="/settings" data-route> 58 + <img src="/static/icons/tool.svg" alt="" aria-hidden="true"> 59 + <span>Settings</span> 60 + </a> 61 + </ui-bottom-bar> 62 + </footer> 63 + </body> 54 64 55 - <footer fixed> 56 - <ui-bottom-bar role="navigation" aria-label="Bottom navigation"> 57 - <a href="/" data-route aria-current="page"> 58 - <img src="/static/icons/home.svg" alt="" aria-hidden="true"> 59 - <span>Map</span> 60 - </a> 61 - <a href="/search" data-route> 62 - <img src="/static/icons/navigation.svg" alt="" aria-hidden="true"> 63 - <span>Search</span> 64 - </a> 65 - <a href="/bookmarks" data-route> 66 - <img src="/static/icons/bookmark.svg" alt="" aria-hidden="true"> 67 - <span>Bookmarks</span> 68 - </a> 69 - <a href="/settings" data-route> 70 - <img src="/static/icons/tool.svg" alt="" aria-hidden="true"> 71 - <span>Settings</span> 72 - </a> 73 - </ui-bottom-bar> 74 - </footer> 75 - </body> 76 65 </html>
+5 -2
www/index.ts
··· 30 30 } 31 31 32 32 const titleEl = document.querySelector('#page-title') 33 - if (titleEl && view.meta?.title !== undefined) { 33 + const header = document.querySelector<HTMLDivElement>('header') 34 + 35 + if (titleEl && view.meta?.title != undefined) { 34 36 titleEl.textContent = view.meta.title 35 37 } 38 + if (header) header.hidden = view.meta?.title == undefined 36 39 37 40 document.querySelectorAll('ui-bottom-bar a').forEach((link) => { 38 41 if ( ··· 58 61 routes: { 59 62 '/': { 60 63 landmarks: { main: 'r-home' }, 61 - meta: { title: 'Maps', navActive: '/' }, 64 + meta: { navActive: '/' }, 62 65 }, 63 66 '/search': { 64 67 landmarks: { main: 'r-search' },
+13 -12
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 ··· 74 74 position: fixed; 75 75 inset: 0; 76 76 z-index: 1; 77 - padding-top: var(--header-height); 78 77 padding-bottom: var(--footer-height); 79 78 box-sizing: border-box; 80 79 } 81 80 82 - m-map[hidden] { 81 + m-map[hidden], 82 + header[hidden] { 83 83 display: none; 84 84 } 85 85 ··· 99 99 z-index: 100; 100 100 } 101 101 102 - m-map > .download-btn { 102 + m-map>.download-btn { 103 103 z-index: 10; 104 104 } 105 105 ··· 232 232 margin-bottom: var(--s3); 233 233 } 234 234 235 - r-settings section > p, 236 - r-settings-downloads section > p, 237 - r-settings-about section > p { 235 + r-settings section>p, 236 + r-settings-downloads section>p, 237 + r-settings-about section>p { 238 238 opacity: 0.6; 239 239 margin-bottom: var(--s3); 240 240 font-size: var(--f5); ··· 424 424 transform: none; 425 425 } 426 426 427 - .search-history-item > span { 427 + .search-history-item>span { 428 428 flex: 1; 429 429 min-width: 0; 430 430 overflow: hidden; ··· 432 432 white-space: nowrap; 433 433 } 434 434 435 - .search-history-item > img { 435 + .search-history-item>img { 436 436 flex-shrink: 0; 437 437 opacity: 0.35; 438 438 } ··· 754 754 border-radius: var(--br-base); 755 755 } 756 756 757 - .bm-import-group + .bm-import-group { 757 + .bm-import-group+.bm-import-group { 758 758 border-top: 1px solid currentColor; 759 759 } 760 760 ··· 775 775 gap: 1px; 776 776 } 777 777 778 - .bm-import-item + .bm-import-item { 778 + .bm-import-item+.bm-import-item { 779 779 border-top: 1px solid color-mix(in srgb, currentColor 15%, transparent); 780 780 } 781 781 ··· 792 792 } 793 793 794 794 @media (max-width: 768px) { 795 + 795 796 input, 796 797 textarea, 797 798 select { ··· 807 808 808 809 .zoom-display { 809 810 position: absolute; 810 - top: 80px; 811 + top: 10px; 811 812 left: 10px; 812 813 background: rgba(255, 255, 255, 0.9); 813 814 padding: 4px 8px;
+10 -11
www/utils/layers.ts
··· 789 789 * regional tile loads is as seamless as possible. 790 790 */ 791 791 export const WORLD_LAYERS = [ 792 - // ── Ocean / Water (fill) ────────────────────────────────────────────────── 793 - // fill-antialias matches regional water layer (no false here) 794 - { 795 - 'id': 'water', 796 - 'type': 'fill', 797 - 'source': 'world', 798 - 'source-layer': 'water', 799 - 'paint': { 'fill-color': COLOR_WATER }, 800 - }, 801 - 802 - // ── Landcover — colours match layers.ts exactly ─────────────────────────── 792 + // ── Landcover — renders below water so coastline-overlapping polygons are masked 803 793 { 804 794 'id': 'landcover', 805 795 'type': 'fill', ··· 839 829 [...MATCH, 'ice', 0.5, 'wood', 0.8, 'sand', 0.3, 'grass', 0.45, 0.5], 840 830 ], 841 831 }, 832 + }, 833 + 834 + // ── Ocean / Water (fill) — above landcover to mask OSM polygons that extend past coastlines 835 + { 836 + 'id': 'water', 837 + 'type': 'fill', 838 + 'source': 'world', 839 + 'source-layer': 'water', 840 + 'paint': { 'fill-color': COLOR_WATER }, 842 841 }, 843 842 844 843 // ── Parks / nature reserves ───────────────────────────────────────────────