also looking at this
fix: resolve #6259 Offline: document and support caching the app sh...
#6270
1 file
+1
-130
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -4,133 +4,4 @@ Download an area once and the map, search and directions keep working there with | ||
| 4 | 4 | |
| 5 | 5 | ## Offline maps |
| 6 | 6 | |
| 7 | The quickest route is the control. It adds a download button to the map: | |
| 8 | ||
| 9 | ```ts | |
| 10 | import { control } from 'ts-maps' | |
| 11 | ||
| 12 | control.offlineMaps().addTo(map) | |
| 13 | ``` | |
| 14 | ||
| 15 | The button opens **Offline Maps**, a list of what has been downloaded, each with its size, date, and a progress bar while it downloads. Each row has Pause/Resume, Update and Delete; Delete asks once more before it deletes. Tapping a name flies to the area and outlines it. | |
| 16 | ||
| 17 | **Download New Map** dims the map around a rounded frame. Drag its corners, or move the map beneath it, to choose the area. The card below names the area after the most important place labelled inside it (pass `geocoder` to name it by reverse geocoding instead), and shows an estimated size before anything is fetched. The estimate comes from sampling a few of the area's own tiles. | |
| 18 | ||
| 19 | When the browser goes offline, a pill at the top says so, and says whether a downloaded map covers the view. The **Only Use Offline Maps** switch keeps the map off the network entirely. | |
| 20 | ||
| 21 | | Option | Default | | | |
| 22 | | --- | --- | --- | | |
| 23 | | `position` | `'topright'` | | | |
| 24 | | `maps` | `offlineMaps()` | The manager to show. | | |
| 25 | | `geocoder` | — | Names a new area by reverse geocoding its centre. | | |
| 26 | | `resources` | — | Other URLs to keep with every download, such as a TileJSON your page fetches itself. | | |
| 27 | | `showStatus` | `true` | The offline pill. | | |
| 28 | ||
| 29 | Every framework binding has it as `<OfflineMaps>`, with `open` and | |
| 30 | `onlyOffline` followed as props — see | |
| 31 | [framework bindings](../guide/framework-bindings.md#offline-maps). | |
| 32 | ||
| 33 | ### From code | |
| 34 | ||
| 35 | `map.offline` is the manager behind the control, and `offlineMaps()` returns the same object: | |
| 36 | ||
| 37 | ```ts | |
| 38 | // What an area would cost, before downloading it. | |
| 39 | const { tiles, bytes, tooLarge } = await map.offline.estimate({ bounds: [-122.45, 37.76, -122.39, 37.81] }) | |
| 40 | ||
| 41 | // Download it. Resolves when it is complete, paused or cancelled. | |
| 42 | map.offline.on('progress', ({ region }) => console.log(region.downloaded, '/', region.tiles)) | |
| 43 | const region = await map.offline.download({ bounds: [-122.45, 37.76, -122.39, 37.81], name: 'San Francisco' }) | |
| 44 | ||
| 45 | await map.offline.list() // every downloaded map, newest first | |
| 46 | await map.offline.pause(region.id) | |
| 47 | await map.offline.resume(region.id) // carries on from where it stopped | |
| 48 | await map.offline.update(region.id) // fetches every tile again | |
| 49 | await map.offline.rename(region.id, 'Home') | |
| 50 | await map.offline.delete(region.id) | |
| 51 | await map.offline.usage() // { bytes, entries } on this device | |
| 52 | ``` | |
| 53 | ||
| 54 | - **Which tiles are downloaded.** An area is downloaded for every tile layer on the map. Each layer is asked for its URLs exactly as it asks while drawing: subdomain, retina suffix and 512px zoom shift included. What is stored is therefore exactly what the map later requests. | |
| 55 | - **Zoom range.** A vector source is kept to its top zoom by default; past that, its tiles are drawn sharply by overzooming. Image layers stop at zoom 16 unless `maxZoom` says otherwise. | |
| 56 | - **Style files.** The style's sprite sheets, the common glyph ranges, and the style document (when it was loaded from a URL) are kept too. | |
| 57 | - **Without a map.** Download from URL templates by passing `sources: [{ url, type, tileSize, maxZoom }]`. | |
| 58 | - **Size limit.** An area over `maxTiles` (150,000 by default) is refused; `estimate()` reports it as `tooLarge`. | |
| 59 | ||
| 60 | Downloads are kept in IndexedDB and survive a reload. A download interrupted by a reload comes back paused. Tiles that two areas share are stored once, and deleting one area keeps whatever the other still needs. Events on the manager: `change`, `progress`, `complete`, `error` and `delete`. | |
| 61 | ||
| 62 | ### How the map uses them | |
| 63 | ||
| 64 | Every tile, style, sprite, glyph and terrain request checks the downloaded maps first. A hit costs no network and no data. A miss goes to the network as usual, or fails straight away when `onlyOffline` is set: | |
| 65 | ||
| 66 | ```ts | |
| 67 | map.offline.onlyOffline = true // Apple's "Only Use Offline Maps" | |
| 68 | map.offline.enabled = false // ignore downloads altogether | |
| 69 | ``` | |
| 70 | ||
| 71 | A page that has never downloaded anything never opens a database: where the browser can say none exists, none is created. | |
| 72 | ||
| 73 | ### Search and directions offline | |
| 74 | ||
| 75 | Downloading reads each vector tile at its top zoom for: | |
| 76 | ||
| 77 | - the places, streets and points of interest named in it; | |
| 78 | - its road network, with one-way streets, ramps, bridges and tunnels. | |
| 79 | ||
| 80 | Both are available as ordinary providers: | |
| 81 | ||
| 82 | ```ts | |
| 83 | import { withOfflineFallback } from 'ts-maps' | |
| 84 | import { NominatimGeocoder, OSRMDirections } from 'ts-maps/services' | |
| 85 | ||
| 86 | const geocoder = withOfflineFallback(new NominatimGeocoder(), map.offline.geocoder()) | |
| 87 | const directions = withOfflineFallback(new OSRMDirections(), map.offline.directions()) | |
| 88 | ``` | |
| 89 | ||
| 90 | `withOfflineFallback` uses the online provider when it can. It switches to the offline one when the browser reports no connection, or when the online request fails for any reason other than being cancelled. | |
| 91 | ||
| 92 | The offline router runs A* over the downloaded roads for driving, walking or cycling, with alternatives. Its steps use the same maneuver codes as the online providers, so `turnByTurn`, the banner and the voice work unchanged: "Turn left onto Broadway", "Turn right onto Van Ness Avenue". | |
| 93 | ||
| 94 | Tiles are simplified when they are made, which drops junction vertices along straight streets. The graph finds those junctions again: at every crossing, and wherever a road ends on another. Roads at different levels are never joined, so a bridge does not connect to the street below it. | |
| 95 | ||
| 96 | Search, reverse geocoding and routing read the OpenMapTiles schema used by the built-in styles. | |
| 97 | ||
| 98 | ## Tile cache | |
| 99 | ||
| 100 | `TileCache` is a lower-level, promise-based key/value store of tile bytes keyed by URL, with optional TTL and LRU limits. It is in memory unless you give it a backend. `cachedFetch` reads through it: a hit returns immediately, and a miss goes to the network and is stored. If the network fails, it falls back to whatever the cache holds. | |
| 101 | ||
| 102 | ```ts | |
| 103 | import { cachedFetch, TileCache } from 'ts-maps' | |
| 104 | ||
| 105 | const cache = new TileCache({ maxBytes: 200 * 1024 * 1024 }) | |
| 106 | const { data, fromCache } = await cachedFetch('https://tile.openstreetmap.org/10/301/384.png', { cache }) | |
| 107 | ``` | |
| 108 | ||
| 109 | Tile layers accept `offlineCache: cache` (or `true` for the shared `getDefaultCache()`) to read through one. Downloaded offline maps are always consulted first. | |
| 110 | ||
| 111 | ### Lifecycle | |
| 112 | ||
| 113 | `close()` releases the backend. It is idempotent, and the cache stays usable: the next `get` / `put` reopens it. For the shared singleton, use `resetDefaultCache()`. | |
| 114 | ||
| 115 | ### Pluggable backends | |
| 116 | ||
| 117 | `new TileCache({ backend })` accepts any object implementing `get` / `put` / `delete` / `clear` / `all` / optional `close`. This is how `@craft-native/ts-maps` stores tiles in the Craft sandbox filesystem. Offline maps have their own store contract, `OfflineStore`, with an IndexedDB and an in-memory implementation; pass one as `new OfflineMaps({ store })`. | |
| 118 | ||
| 119 | ### One-shot prefetch | |
| 120 | ||
| 121 | `saveOfflineRegion({ bounds, zoomRange, tileUrl, cache })` fills a `TileCache` with every tile of one URL template in a bounding box. It predates offline maps: it keeps no record of the area, and the area cannot be paused, resumed, updated or deleted. Prefer `map.offline.download()`. | |
| 122 | ||
| 123 | ## Worker pool | |
| 124 | ||
| 125 | `WorkerPool` round-robins jobs across a fixed number of Web Workers. Vector tile decode, heatmap rasterisation, and hillshade computation all route through it by default. | |
| 126 | ||
| 127 | ```ts | |
| 128 | import { WorkerPool } from 'ts-maps' | |
| 129 | ||
| 130 | const pool = new WorkerPool({ size: navigator.hardwareConcurrency ?? 4 }) | |
| 131 | const decoded = await pool.run('decodeMvt', { bytes, extent: 4096 }) | |
| 132 | ``` | |
| 133 | ||
| 134 | ## Try it | |
| 135 | ||
| 136 | The playground's **14. Offline maps** page has the whole flow. Pick an area, download it, then either switch on "Only Use Offline Maps" or take the browser offline; the downloaded area keeps drawing. The older [offline example](../examples/11-offline.md) shows the lower-level `TileCache` and region prefetcher on their own. | |
| 7 | The quickest route is the control. It adds a download button to the map: | |