|
2 | 2 |
|
3 | 3 | *A local-first travel photo mapping app. Runs entirely in your browser, no account needed.* |
4 | 4 |
|
| 5 | + |
| 6 | + |
| 7 | +<details> |
| 8 | +<summary>Dark Map</summary> |
| 9 | + |
| 10 | + |
| 11 | +</details> |
| 12 | + |
| 13 | +<details> |
| 14 | +<summary>Pin a Location</summary> |
| 15 | + |
| 16 | + |
| 17 | +</details> |
| 18 | + |
5 | 19 | ## Quick Start |
6 | 20 |
|
7 | 21 | 1. **Download** the repository (git clone it). |
|
21 | 35 | or click it to browse your drive. |
22 | 36 |
|
23 | 37 | --- |
24 | | -<img width="3438" height="1692" alt="image" src="https://github.com/user-attachments/assets/4d802eac-8328-4dcf-85ab-7dfa9999a584" /> |
25 | | - |
26 | | ---- |
27 | | -<img width="810" height="686" alt="animated" src="https://github.com/user-attachments/assets/38eccc4b-95bb-4b9f-a668-b125b30e2d5e" /> |
28 | 38 |
|
29 | | ---- |
30 | | -<img width="988" height="604" alt="animated_panning" src="https://github.com/user-attachments/assets/a377f963-a288-4bce-bc78-f2f24ca52f20" /> |
31 | | - |
32 | | ---- |
33 | 39 | [](https://www.youtube.com/watch?v=w9MhBRoiUho) |
34 | 40 |
|
35 | 41 | --- |
| 42 | + |
36 | 43 | ## Features |
37 | 44 |
|
38 | 45 | | Feature | Detail | |
|
45 | 52 | | 🗓 Timeline | Browse pinned photos chronologically | |
46 | 53 | | 🖼 Lightbox | Full-size photo viewer with smooth navigation animations | |
47 | 54 | | 📝 Notes | Add notes to any pin location | |
48 | | -| 🛰 Map styles | Light, Dark, Terrain (natural earth shading), and Satellite (Esri) | |
| 55 | +| 🛰 Map styles | Light, Dark, Terrain, 3D Terrain, Satellite, and Globe | |
49 | 56 | | 🗺 Vector tiles | Smooth zoom with no tile flickering (OpenFreeMap Liberty) | |
50 | 57 | | 🔄 Clustering | Nearby pins cluster automatically, expand on zoom | |
51 | 58 | | 💾 Auto-save | Automatic backup to disk when running via `serve.py` | |
|
61 | 68 |
|
62 | 69 | ## Tile Caching |
63 | 70 |
|
64 | | -Map tiles are cached in three layers for fast, offline-capable rendering: |
| 71 | +Map tiles are cached in a multi-layer architecture designed for fast rendering and offline access: |
| 72 | + |
| 73 | +### Storage Layers |
65 | 74 |
|
66 | | -| Layer | Storage | Speed | Scope | Persistence | |
| 75 | +| Layer | What it is | Speed | Persistence | Browser support | |
67 | 76 | |---|---|---|---|---| |
68 | | -| **L1 — SW Cache API** | Browser (via service worker) | Instant | Per-browser | Cleared with browser data | |
69 | | -| **L2 — Disk cache** | `matrix-tiles/` on disk (via `serve.py`) | Fast local read | Shared across all browsers | Persists until manually deleted | |
70 | | -| **L3 — Origin fetch** | Remote tile server (OpenFreeMap / Esri) | Slowest | Requires internet | N/A | |
| 77 | +| **L1 — Cache API** | Browser-side HTTP response cache, managed by the service worker | Instant (~0ms) | Cleared with browser data | Chrome, Firefox (not Safari) | |
| 78 | +| **L2 — Disk cache** | Local filesystem at `matrix-tiles/`, served by `serve.py` | Fast (~5-10ms) | Persists until manually deleted or evicted | All browsers | |
| 79 | +| **L3 — Origin** | Remote tile servers (OpenFreeMap, ArcGIS) | Network-dependent (~50-200ms) | N/A | All browsers | |
| 80 | + |
| 81 | +### How it works |
| 82 | + |
| 83 | +**Chrome / Firefox (with service worker):** |
| 84 | + |
| 85 | +``` |
| 86 | +MapLibre requests tile |
| 87 | + → SW intercepts |
| 88 | + → L1 check (Cache API) — instant hit if previously fetched |
| 89 | + → L1 miss: race L2 (disk proxy) and L3 (origin) via Promise.any |
| 90 | + → Whichever responds first wins |
| 91 | + → Result stored in L1 for future requests |
| 92 | + → Origin fetches saved to L2 in background |
| 93 | +``` |
| 94 | + |
| 95 | +**Safari (no service worker):** |
| 96 | + |
| 97 | +Safari's service worker implementation has persistent issues (premature context termination, stale caches, failed tile loads). The SW is intentionally disabled in Safari. Tiles flow directly: |
| 98 | + |
| 99 | +``` |
| 100 | +MapLibre requests tile |
| 101 | + → Browser fetches from origin (L3) |
| 102 | + → Proactive caching (data.js) prefetches tiles via serve.py |
| 103 | + → Disk cache (L2) available for offline use via serve.py proxy |
| 104 | +``` |
| 105 | + |
| 106 | +### Data Storage (separate from tile caching) |
71 | 107 |
|
72 | | -On an L1 miss, the service worker checks L2 (disk proxy) first with a 50ms timeout. If the tile is on disk, it's served immediately with no origin request. If the disk check misses or times out, the service worker falls back to L3 (origin) with an 8-second timeout and one automatic retry on failure. Concurrency is limited by semaphores (4 concurrent disk, 6 concurrent origin) to prevent overwhelming either backend during rapid zoom transitions. After a successful origin fetch, the tile is saved to disk in the background for offline use. |
| 108 | +| Storage | What it stores | Used by | |
| 109 | +|---|---|---| |
| 110 | +| **IndexedDB** | Photos, albums, metadata, thumbnails, geo caches | App data layer (`dbPut()` / `dbGetAll()`) | |
| 111 | +| **Disk (matrix-data.json)** | Auto-save backup of all app data | `serve.py` auto-save endpoint | |
| 112 | +| **Disk (matrix-photos/)** | Full-size images and thumbnails | `serve.py` photo storage | |
73 | 113 |
|
74 | | -**URL-based versioning:** Tile URLs include a version segment (e.g., `20260415_001001_pt`) that changes when OpenFreeMap rebuilds their tile set. This means cached tiles are never stale — when tiles are updated, the style JSON points to new URLs, the cache naturally misses, and fresh tiles are fetched and cached. Old versioned tiles are eventually removed by LRU eviction. |
| 114 | +IndexedDB and photo storage are unaffected by the service worker — app data persists identically in all browsers. |
75 | 115 |
|
76 | | -The disk cache is capped at 500 MB with LRU eviction — when the limit is exceeded, the oldest tiles are removed down to 80% capacity. Eviction runs at startup and after each new tile is cached. Eviction events are logged to `matrix-requests.log`. |
| 116 | +### Tile cache configuration |
77 | 117 |
|
78 | | -**Proactive caching:** After app load, tiles for the world overview (z0–3) and pinned photo locations (z4–14) are prefetched in small batches. Already-cached tiles are skipped to avoid redundant network requests. |
| 118 | +- **Disk cache limit:** 500 MB with LRU eviction (oldest tiles removed down to 80% when limit exceeded) |
| 119 | +- **Eviction runs:** at startup and after each new tile is cached |
| 120 | +- **Eviction logging:** written to `matrix-requests.log` |
| 121 | +- **SW Cache API limit:** 10,000 entries with zoom-aware LRU (low-zoom tiles z≤8 protected from eviction) |
| 122 | + |
| 123 | +### URL-based versioning |
| 124 | + |
| 125 | +Tile URLs include a version segment (e.g., `20260415_001001_pt`) that changes when OpenFreeMap rebuilds their tile set. Cached tiles are never stale — when tiles update, the style JSON points to new URLs, the cache naturally misses, and fresh tiles are fetched. Old versioned tiles are eventually evicted by LRU. |
| 126 | + |
| 127 | +### Proactive caching |
| 128 | + |
| 129 | +After app load (10s delay), tiles for the world overview (z0–3) and pinned photo locations (z4–14) are prefetched in small batches. Already-cached tiles are skipped. This runs in the background without blocking interactive map use. |
79 | 130 |
|
80 | 131 | ## Video Export |
81 | 132 |
|
@@ -128,13 +179,24 @@ The app uses three separate services that work together to render interactive ma |
128 | 179 | - **OpenFreeMap** — the tile server. Takes OSM's raw data, renders it into vector map tiles (`.pbf` files), and serves them alongside style definitions (JSON files that describe how to color roads, label cities, etc.). Free, no API key required. The app uses its `liberty` style (light) and `dark` style. |
129 | 180 | - **MapLibre GL JS** — the client-side rendering engine. A JavaScript library that takes tiles and style JSON from OpenFreeMap and renders an interactive, zoomable map on a `<canvas>` element in the browser. Handles panning, zooming, markers, clusters, and all map interaction. |
130 | 181 |
|
131 | | -The satellite view uses **ArcGIS World Imagery** (Esri) as a separate raster tile source, unrelated to the OSM ecosystem. |
| 182 | +The app offers six map styles: |
| 183 | + |
| 184 | +| Style | Description | |
| 185 | +|---|---| |
| 186 | +| **Light Map** | Clean vector map with muted colors | |
| 187 | +| **Dark Map** | Dark-themed vector map with normalized labels | |
| 188 | +| **Terrain** | Light map with natural-earth shaded relief raster overlay | |
| 189 | +| **3D Terrain** | True 3D elevation via AWS Terrain Tiles with hillshading. Pitch/bearing/exaggeration controls appear at bottom-left. Right-click shows elevation in meters. | |
| 190 | +| **Satellite** | ArcGIS World Imagery raster tiles (Esri) | |
| 191 | +| **Globe** | Spherical globe projection — pan to see the whole Earth | |
| 192 | + |
| 193 | +The satellite and 3D Terrain views use separate raster tile sources unrelated to the OpenFreeMap/OSM ecosystem. 3D Terrain elevation data comes from **AWS Terrain Tiles** (free, no API key, terrarium encoding), capped at zoom 15 for maximum detail. |
132 | 194 |
|
133 | 195 | **Nominatim** (run by OpenStreetMap) is used for geocoding — converting place names to coordinates. Requests are rate-limited to 1 per second per their usage policy. |
134 | 196 |
|
135 | 197 | ## Privacy |
136 | 198 |
|
137 | | -Everything stays **100% local**. No data is sent anywhere except OpenStreetMap/Nominatim for place lookups. No login required. |
| 199 | +Everything stays **100% local**. No data is sent anywhere except OpenStreetMap/Nominatim for place lookups. No login required. |
138 | 200 |
|
139 | 201 | --- |
140 | 202 |
|
|
0 commit comments