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: update command should rebuild data each time.

+80 -72
+61 -60
data/cli/commands/update.ts
··· 1 1 import { Command } from '@cliffy/command' 2 - import { join } from '@std/path' 3 - import { leafName } from '../shared/regions.ts' 4 - import { POLY_DIR, TILES_MANIFEST } from '../shared/paths.ts' 2 + import { walk } from '@std/fs' 3 + import { join, relative } from '@std/path' 4 + import { POLY_DIR, TILES_MANIFEST, TILES_OUT_DIR } from '../shared/paths.ts' 5 5 6 6 type TileEntry = { 7 7 id: string ··· 20 20 bounds?: [number, number, number, number] 21 21 } 22 22 23 + function slugToLabel(slug: string): string { 24 + if (slug === 'us') return 'US' 25 + return slug.split('-').map((w) => w[0].toUpperCase() + w.slice(1)).join(' ') 26 + } 27 + 28 + function formatSize(bytes: number): string { 29 + if (bytes < 1024 * 1024) { 30 + return `~${Math.round(bytes / 1024)} KB` 31 + } 32 + return `~${Math.round(bytes / (1024 * 1024))} MB` 33 + } 34 + 23 35 async function bboxFromPoly( 24 36 region: string, 25 37 ): Promise<[number, number, number, number] | null> { ··· 27 39 const polyPath = join( 28 40 POLY_DIR, 29 41 ...parts.slice(0, -1), 30 - `${leafName(region)}.poly`, 42 + `${parts[parts.length - 1]}.poly`, 31 43 ) 32 44 try { 33 45 const text = await Deno.readTextFile(polyPath) ··· 48 60 return null 49 61 } 50 62 51 - async function updateManifestBounds(): Promise<void> { 52 - let tiles: TileEntry[] = [] 53 - try { 54 - tiles = JSON.parse(await Deno.readTextFile(TILES_MANIFEST)) 55 - } catch { 56 - throw new Error(`No tiles manifest found at ${TILES_MANIFEST}`) 57 - } 58 - 59 - let updated = 0 60 - for (const tile of tiles) { 61 - if (tile.bounds) continue 62 - 63 - const groupParts = tile.group ? tile.group.split('/') : [] 64 - const region = [...groupParts, tile.id].join('/') 65 - const bounds = await bboxFromPoly(region) 66 - 67 - if (bounds) { 68 - tile.bounds = bounds 69 - updated++ 70 - } 71 - } 72 - 73 - const groupEntries = buildGroupEntries(tiles) 74 - 75 - const allEntries = [...tiles, ...groupEntries] 76 - 77 - await Deno.writeTextFile( 78 - TILES_MANIFEST, 79 - JSON.stringify(allEntries, null, 2) + '\n', 80 - ) 81 - 82 - console.log(`Updated ${updated} entries with bounds`) 83 - console.log(`Added ${groupEntries.length} group entries`) 84 - } 85 - 86 - function slugToLabel(slug: string): string { 87 - if (slug === 'us') return 'US' 88 - return slug.split('-').map((w) => w[0].toUpperCase() + w.slice(1)).join(' ') 89 - } 90 - 91 63 function buildGroupEntries(tiles: TileEntry[]): GroupEntry[] { 92 64 const childGroups = new Set<string>() 93 - 94 65 for (const tile of tiles) { 95 - if (tile.group) { 96 - childGroups.add(tile.group) 97 - } 66 + if (tile.group) childGroups.add(tile.group) 98 67 } 99 68 100 69 const groupEntries: GroupEntry[] = [] 101 - const addedGroupPaths = new Set<string>() 70 + const seen = new Set<string>() 102 71 103 72 for (const childGroup of childGroups) { 104 73 const parts = childGroup.split('/') ··· 106 75 107 76 const parentGroup = parts.slice(0, -1).join('/') 108 77 const id = parts[parts.length - 1] 109 - const groupPath = `${parentGroup}/${id}` 110 - 111 - if (addedGroupPaths.has(groupPath)) continue 112 - addedGroupPaths.add(groupPath) 78 + const key = `${parentGroup}/${id}` 79 + if (seen.has(key)) continue 80 + seen.add(key) 113 81 114 82 const groupTiles = tiles.filter((t) => t.group === childGroup) 115 - 116 83 let west = 180, south = 90, east = -180, north = -90 117 84 let hasBounds = false 118 - 119 85 for (const tile of groupTiles) { 120 86 if (tile.bounds) { 121 87 const [w, s, e, n] = tile.bounds ··· 138 104 return groupEntries 139 105 } 140 106 107 + async function rebuildManifest(): Promise<void> { 108 + const tiles: TileEntry[] = [] 109 + 110 + for await (const entry of walk(TILES_OUT_DIR)) { 111 + if (!entry.isFile || !entry.name.endsWith('.pmtiles')) continue 112 + 113 + const rel = relative(TILES_OUT_DIR, entry.path) 114 + const parts = rel.split('/') 115 + const basename = parts[parts.length - 1].replace('.pmtiles', '') 116 + const id = basename.replaceAll('_', '-') 117 + const group = parts.slice(0, -1).join('/') 118 + 119 + const { size } = await Deno.stat(entry.path) 120 + const region = [...parts.slice(0, -1), basename].join('/') 121 + const bounds = await bboxFromPoly(region) 122 + 123 + tiles.push({ 124 + id, 125 + label: slugToLabel(id), 126 + description: `Detailed street map · ${formatSize(size)}`, 127 + filename: rel, 128 + path: `/static/tiles/${rel}`, 129 + group, 130 + bounds: bounds ?? undefined, 131 + }) 132 + } 133 + 134 + tiles.sort((a, b) => a.filename.localeCompare(b.filename)) 135 + 136 + const groupEntries = buildGroupEntries(tiles) 137 + const allEntries = [...tiles, ...groupEntries] 138 + 139 + await Deno.writeTextFile(TILES_MANIFEST, JSON.stringify(allEntries) + '\n') 140 + 141 + console.log(`Wrote ${tiles.length} tiles and ${groupEntries.length} groups`) 142 + } 143 + 141 144 export const updateCmd = new Command() 142 145 .name('update') 143 146 .description( 144 - 'Update tiles.json with computed bounds from poly files. ' + 145 - 'Does not rebuild tiles.', 147 + 'Rebuild tiles.json from .pmtiles files in www/static/tiles. ' + 148 + 'Overwrites the entire manifest with entries derived from the file tree.', 146 149 ) 147 - .action(async () => { 148 - await updateManifestBounds() 149 - }) 150 + .action(rebuildManifest)
+1 -1
deno.json
··· 1 1 { 2 - "version": "0.3.2", 2 + "version": "0.4.0", 3 3 "workspace": ["./data"], 4 4 "tasks": { 5 5 "data": "deno run -A ./data/cli/main.ts",
+6 -1
www/components/m-map.ts
··· 69 69 if (changedProperties.has('hidden') && !this.hidden && this.#map) { 70 70 this.#map.resize() 71 71 } 72 + if (globalThis.__DEV__) { 73 + const zoom = document.querySelector<HTMLElement>('.zoom-display') 74 + if (zoom) zoom.hidden = false 75 + console.log(zoom) 76 + } 72 77 } 73 78 74 79 #onMoveEnd = debounce(() => { ··· 682 687 683 688 return html` 684 689 <div id="map"></div> 685 - <div class="zoom-display" ${globalThis.__DEV__ ? '' : 'hidden'}> 690 + <div class="zoom-display" hidden> 686 691 Zoom: ${this.#currentZoom.toFixed(1)} 687 692 </div> 688 693 ${tile
+12 -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 ··· 79 79 } 80 80 81 81 m-map[hidden], 82 - header[hidden] { 82 + header[hidden], 83 + .zoom-display[hidden] { 83 84 display: none; 84 85 } 85 86 ··· 99 100 z-index: 100; 100 101 } 101 102 102 - m-map > .download-btn { 103 + m-map>.download-btn { 103 104 z-index: 10; 104 105 } 105 106 ··· 232 233 margin-bottom: var(--s3); 233 234 } 234 235 235 - r-settings section > p, 236 - r-settings-downloads section > p, 237 - r-settings-about section > p { 236 + r-settings section>p, 237 + r-settings-downloads section>p, 238 + r-settings-about section>p { 238 239 opacity: 0.6; 239 240 margin-bottom: var(--s3); 240 241 font-size: var(--f5); ··· 424 425 transform: none; 425 426 } 426 427 427 - .search-history-item > span { 428 + .search-history-item>span { 428 429 flex: 1; 429 430 min-width: 0; 430 431 overflow: hidden; ··· 432 433 white-space: nowrap; 433 434 } 434 435 435 - .search-history-item > img { 436 + .search-history-item>img { 436 437 flex-shrink: 0; 437 438 opacity: 0.35; 438 439 } ··· 754 755 border-radius: var(--br-base); 755 756 } 756 757 757 - .bm-import-group + .bm-import-group { 758 + .bm-import-group+.bm-import-group { 758 759 border-top: 1px solid currentColor; 759 760 } 760 761 ··· 775 776 gap: 1px; 776 777 } 777 778 778 - .bm-import-item + .bm-import-item { 779 + .bm-import-item+.bm-import-item { 779 780 border-top: 1px solid color-mix(in srgb, currentColor 15%, transparent); 780 781 } 781 782 ··· 792 793 } 793 794 794 795 @media (max-width: 768px) { 796 + 795 797 input, 796 798 textarea, 797 799 select {