styled-map-package 2.2.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/bin/smp-download.js +3 -3
  2. package/bin/smp-mbtiles.js +1 -1
  3. package/bin/smp-view.js +2 -2
  4. package/dist/download.cjs +101 -0
  5. package/dist/download.d.cts +65 -0
  6. package/dist/{lib/download.d.ts → download.d.ts} +22 -6
  7. package/dist/download.js +77 -0
  8. package/dist/from-mbtiles.cjs +91 -0
  9. package/dist/from-mbtiles.d.cts +17 -0
  10. package/dist/from-mbtiles.d.ts +17 -0
  11. package/dist/from-mbtiles.js +57 -0
  12. package/dist/index.cjs +49 -0
  13. package/dist/index.d.cts +27 -0
  14. package/dist/index.d.ts +27 -0
  15. package/dist/index.js +18 -0
  16. package/dist/reader-watch.cjs +135 -0
  17. package/dist/reader-watch.d.cts +24 -0
  18. package/dist/reader-watch.d.ts +24 -0
  19. package/dist/reader-watch.js +101 -0
  20. package/dist/reader.cjs +167 -0
  21. package/dist/reader.d.cts +62 -0
  22. package/dist/{lib/reader.d.ts → reader.d.ts} +16 -5
  23. package/dist/reader.js +138 -0
  24. package/dist/reporters.cjs +122 -0
  25. package/dist/reporters.d.cts +10 -0
  26. package/dist/{lib/reporters.d.ts → reporters.d.ts} +5 -2
  27. package/dist/reporters.js +88 -0
  28. package/dist/server.cjs +79 -0
  29. package/dist/server.d.cts +37 -0
  30. package/dist/server.d.ts +37 -0
  31. package/dist/server.js +45 -0
  32. package/dist/style-downloader.cjs +312 -0
  33. package/dist/style-downloader.d.cts +120 -0
  34. package/dist/{lib/style-downloader.d.ts → style-downloader.d.ts} +23 -13
  35. package/dist/style-downloader.js +288 -0
  36. package/dist/tile-downloader.cjs +158 -0
  37. package/dist/tile-downloader.d.cts +84 -0
  38. package/dist/{lib/tile-downloader.d.ts → tile-downloader.d.ts} +22 -10
  39. package/dist/tile-downloader.js +126 -0
  40. package/dist/{lib/writer.d.ts → types-B4Xn1F9K.d.cts} +74 -14
  41. package/dist/types-B4Xn1F9K.d.ts +189 -0
  42. package/dist/utils/errors.cjs +41 -0
  43. package/dist/utils/errors.d.cts +18 -0
  44. package/dist/{lib/utils → utils}/errors.d.ts +4 -2
  45. package/dist/utils/errors.js +16 -0
  46. package/dist/utils/fetch.cjs +96 -0
  47. package/dist/utils/fetch.d.cts +51 -0
  48. package/dist/{lib/utils → utils}/fetch.d.ts +10 -4
  49. package/dist/utils/fetch.js +62 -0
  50. package/dist/utils/file-formats.cjs +98 -0
  51. package/dist/utils/file-formats.d.cts +35 -0
  52. package/dist/{lib/utils → utils}/file-formats.d.ts +13 -3
  53. package/dist/utils/file-formats.js +62 -0
  54. package/dist/utils/geo.cjs +84 -0
  55. package/dist/utils/geo.d.cts +46 -0
  56. package/dist/{lib/utils → utils}/geo.d.ts +8 -6
  57. package/dist/utils/geo.js +56 -0
  58. package/dist/utils/mapbox.cjs +121 -0
  59. package/dist/utils/mapbox.d.cts +43 -0
  60. package/dist/utils/mapbox.d.ts +43 -0
  61. package/dist/utils/mapbox.js +91 -0
  62. package/dist/utils/misc.cjs +39 -0
  63. package/{lib/utils/misc.js → dist/utils/misc.d.cts} +5 -9
  64. package/dist/{lib/utils → utils}/misc.d.ts +5 -3
  65. package/dist/utils/misc.js +13 -0
  66. package/dist/utils/streams.cjs +130 -0
  67. package/dist/utils/streams.d.cts +73 -0
  68. package/dist/{lib/utils → utils}/streams.d.ts +14 -10
  69. package/dist/utils/streams.js +103 -0
  70. package/dist/utils/style.cjs +126 -0
  71. package/dist/utils/style.d.cts +69 -0
  72. package/dist/{lib/utils → utils}/style.d.ts +19 -9
  73. package/dist/utils/style.js +98 -0
  74. package/dist/utils/templates.cjs +114 -0
  75. package/dist/utils/templates.d.cts +78 -0
  76. package/dist/{lib/utils → utils}/templates.d.ts +24 -14
  77. package/dist/utils/templates.js +79 -0
  78. package/dist/writer.cjs +401 -0
  79. package/dist/writer.d.cts +7 -0
  80. package/dist/writer.d.ts +7 -0
  81. package/dist/writer.js +374 -0
  82. package/package.json +87 -33
  83. package/dist/lib/from-mbtiles.d.ts +0 -13
  84. package/dist/lib/index.d.ts +0 -10
  85. package/dist/lib/reader-watch.d.ts +0 -13
  86. package/dist/lib/server.d.ts +0 -15
  87. package/dist/lib/types.d.ts +0 -64
  88. package/dist/lib/utils/mapbox.d.ts +0 -41
  89. package/lib/download.js +0 -114
  90. package/lib/from-mbtiles.js +0 -83
  91. package/lib/index.js +0 -11
  92. package/lib/reader-watch.js +0 -133
  93. package/lib/reader.js +0 -165
  94. package/lib/reporters.js +0 -92
  95. package/lib/server.js +0 -81
  96. package/lib/style-downloader.js +0 -363
  97. package/lib/tile-downloader.js +0 -188
  98. package/lib/types.ts +0 -104
  99. package/lib/utils/errors.js +0 -24
  100. package/lib/utils/fetch.js +0 -100
  101. package/lib/utils/file-formats.js +0 -85
  102. package/lib/utils/geo.js +0 -87
  103. package/lib/utils/mapbox.js +0 -155
  104. package/lib/utils/streams.js +0 -165
  105. package/lib/utils/style.js +0 -174
  106. package/lib/utils/templates.js +0 -136
  107. package/lib/writer.js +0 -478
package/lib/server.js DELETED
@@ -1,81 +0,0 @@
1
- import createError from 'http-errors'
2
-
3
- import Reader from './reader.js'
4
- import { isFileNotThereError } from './utils/errors.js'
5
- import { noop } from './utils/misc.js'
6
-
7
- /** @import { FastifyPluginCallback, FastifyReply } from 'fastify' */
8
- /** @import { Resource } from './reader.js' */
9
-
10
- /**
11
- * @typedef {object} PluginOptionsFilepath
12
- * @property {string} filepath Path to styled map package (`.smp`) file
13
- */
14
- /**
15
- * @typedef {object} PluginOptionsReader
16
- * @property {Pick<Reader, keyof Reader>} reader SMP Reader interface (also supports ReaderWatch)
17
- */
18
- /**
19
- * @typedef {PluginOptionsFilepath | PluginOptionsReader} PluginOptions
20
- */
21
-
22
- /**
23
- * @param {FastifyReply} reply
24
- * @param {Resource} resource
25
- * @returns {FastifyReply} reply
26
- */
27
- function sendResource(reply, resource) {
28
- reply
29
- .type(resource.contentType)
30
- .header('content-length', resource.contentLength)
31
- if (resource.contentEncoding) {
32
- reply.header('content-encoding', resource.contentEncoding)
33
- }
34
- return reply.send(resource.stream)
35
- }
36
-
37
- /**
38
- * Fastify plugin for serving a styled map package.
39
- *
40
- * If you provide a `Reader` (or `ReaderWatch`) instance via the `reader` opt,
41
- * you must manually close the instance yourself.
42
- *
43
- * @type {FastifyPluginCallback<PluginOptions>}
44
- */
45
- export default function (fastify, opts, done) {
46
- const reader = 'reader' in opts ? opts.reader : new Reader(opts.filepath)
47
-
48
- // Only close the reader if it was created by this plugin
49
- if (!('reader' in opts)) {
50
- fastify.addHook('onClose', () => reader.close().catch(noop))
51
- }
52
-
53
- fastify.get('/style.json', async () => {
54
- try {
55
- const baseUrl = new URL(fastify.prefix, fastify.listeningOrigin)
56
- const style = await reader.getStyle(baseUrl.href)
57
- return style
58
- } catch (error) {
59
- if (isFileNotThereError(error)) {
60
- throw createError(404, error.message)
61
- }
62
- throw error
63
- }
64
- })
65
-
66
- fastify.get('*', async (request, reply) => {
67
- // @ts-expect-error - not worth the hassle of type casting this
68
- const path = request.params['*']
69
-
70
- try {
71
- const resource = await reader.getResource(path)
72
- return sendResource(reply, resource)
73
- } catch (error) {
74
- if (isFileNotThereError(error)) {
75
- throw createError(404, error.message)
76
- }
77
- throw error
78
- }
79
- })
80
- done()
81
- }
@@ -1,363 +0,0 @@
1
- import { check as checkGeoJson } from '@placemarkio/check-geojson'
2
- import { includeKeys } from 'filter-obj'
3
- import ky from 'ky'
4
- import Queue from 'yocto-queue'
5
- import zlib from 'zlib'
6
-
7
- import { downloadTiles } from './tile-downloader.js'
8
- import { FetchQueue } from './utils/fetch.js'
9
- import {
10
- normalizeGlyphsURL,
11
- normalizeSourceURL,
12
- normalizeSpriteURL,
13
- normalizeStyleURL,
14
- } from './utils/mapbox.js'
15
- import { clone, noop } from './utils/misc.js'
16
- import {
17
- assertTileJSON,
18
- isInlinedSource,
19
- mapFontStacks,
20
- validateStyle,
21
- } from './utils/style.js'
22
-
23
- /** @import { SourceSpecification, StyleSpecification } from '@maplibre/maplibre-gl-style-spec' */
24
- /** @import { TileInfo, GlyphInfo, GlyphRange } from './writer.js' */
25
- /** @import { TileDownloadStats } from './tile-downloader.js' */
26
- /** @import { StyleInlinedSources, InlinedSource } from './types.js'*/
27
-
28
- /** @typedef { import('ky').ResponsePromise & { body: ReadableStream<Uint8Array> } } ResponsePromise */
29
- /** @import { DownloadResponse } from './utils/fetch.js' */
30
-
31
- /**
32
- * @typedef {object} GlyphDownloadStats
33
- * @property {number} total
34
- * @property {number} downloaded
35
- * @property {number} totalBytes
36
- */
37
-
38
- /**
39
- * Download a style and its resources for offline use. Please check the terms of
40
- * service of the map provider you are using before downloading any resources.
41
- */
42
- export default class StyleDownloader {
43
- /** @type {null | string} */
44
- #styleURL = null
45
- /** @type {null | StyleSpecification} */
46
- #inputStyle = null
47
- /** @type {FetchQueue} */
48
- #fetchQueue
49
- #mapboxAccessToken
50
-
51
- /**
52
- * @param {string | StyleSpecification} style A url to a style JSON file or a style object
53
- * @param {object} [opts]
54
- * @param {number} [opts.concurrency=8]
55
- * @param {string} [opts.mapboxAccessToken] Downloading a style from Mapbox requires an access token
56
- */
57
- constructor(style, { concurrency = 8, mapboxAccessToken } = {}) {
58
- if (typeof style === 'string') {
59
- const { searchParams } = new URL(style)
60
- this.#mapboxAccessToken =
61
- searchParams.get('access_token') || mapboxAccessToken
62
- this.#styleURL = normalizeStyleURL(style, this.#mapboxAccessToken)
63
- } else if (validateStyle(style)) {
64
- this.#inputStyle = clone(style)
65
- } else {
66
- throw new AggregateError(validateStyle.errors, 'Invalid style')
67
- }
68
- this.#fetchQueue = new FetchQueue(concurrency)
69
- }
70
-
71
- /**
72
- * Number of active downloads.
73
- */
74
- get active() {
75
- return this.#fetchQueue.activeCount
76
- }
77
-
78
- /**
79
- * Download the style JSON for this style and inline the sources
80
- *
81
- * @returns {Promise<StyleInlinedSources>}
82
- */
83
- async getStyle() {
84
- if (!this.#inputStyle && this.#styleURL) {
85
- const downloadedStyle = await ky(this.#styleURL).json()
86
- if (!validateStyle(downloadedStyle)) {
87
- throw new AggregateError(
88
- validateStyle.errors,
89
- 'Invalid style: ' + this.#styleURL,
90
- )
91
- }
92
- this.#inputStyle = downloadedStyle
93
- } else if (!this.#inputStyle) {
94
- throw new Error('Unexpected state: no style or style URL provided')
95
- }
96
- /** @type {{ [_:string]: InlinedSource }} */
97
- const inlinedSources = {}
98
- for (const [sourceId, source] of Object.entries(this.#inputStyle.sources)) {
99
- inlinedSources[sourceId] = await this.#getInlinedSource(source)
100
- }
101
- return {
102
- ...this.#inputStyle,
103
- sources: inlinedSources,
104
- }
105
- }
106
-
107
- /**
108
- * @param {SourceSpecification} source
109
- * @returns {Promise<InlinedSource>}
110
- */
111
- async #getInlinedSource(source) {
112
- if (isInlinedSource(source)) {
113
- return source
114
- }
115
- if (
116
- source.type === 'raster' ||
117
- source.type === 'vector' ||
118
- source.type === 'raster-dem'
119
- ) {
120
- if (!source.url) {
121
- throw new Error('Source is missing both url and tiles properties')
122
- }
123
- const sourceUrl = normalizeSourceURL(source.url, this.#mapboxAccessToken)
124
- const tilejson = await ky(sourceUrl).json()
125
- assertTileJSON(tilejson)
126
- return {
127
- ...source,
128
- ...includeKeys(tilejson, [
129
- 'bounds',
130
- 'maxzoom',
131
- 'minzoom',
132
- 'tiles',
133
- 'description',
134
- 'attribution',
135
- 'vector_layers',
136
- ]),
137
- }
138
- } else if (source.type === 'geojson') {
139
- if (typeof source.data !== 'string') {
140
- // Shouldn't get here because of the `isInlineSource()` check above, but
141
- // Typescript can't fiture that out.
142
- throw new Error('Unexpected data property for GeoJson source')
143
- }
144
- const geojsonUrl = normalizeSourceURL(
145
- source.data,
146
- this.#mapboxAccessToken,
147
- )
148
- const data = checkGeoJson(await ky(geojsonUrl).text())
149
- return {
150
- ...source,
151
- data,
152
- }
153
- }
154
- return source
155
- }
156
-
157
- /**
158
- * Download the sprite PNGs and JSON files for this style. Returns an async
159
- * generator of json and png readable streams, and the sprite id and pixel
160
- * ratio. Downloads pixel ratios `1` and `2`.
161
- *
162
- * @returns {AsyncGenerator<{ json: import('stream').Readable, png: import('stream').Readable, id: string, pixelRatio: number }>}
163
- */
164
- async *getSprites() {
165
- const style = await this.getStyle()
166
- if (!style.sprite) return
167
- const accessToken = this.#mapboxAccessToken
168
- const spriteDefs = Array.isArray(style.sprite)
169
- ? style.sprite
170
- : [{ id: 'default', url: style.sprite }]
171
- for (const { id, url } of spriteDefs) {
172
- for (const pixelRatio of [1, 2]) {
173
- const format = pixelRatio === 1 ? '' : '@2x'
174
- const jsonUrl = normalizeSpriteURL(url, format, '.json', accessToken)
175
- const pngUrl = normalizeSpriteURL(url, format, '.png', accessToken)
176
- const [{ body: json }, { body: png }] = await Promise.all([
177
- this.#fetchQueue.fetch(jsonUrl),
178
- this.#fetchQueue.fetch(pngUrl),
179
- ])
180
- yield { json, png, id, pixelRatio }
181
- }
182
- }
183
- }
184
-
185
- /**
186
- * Download all the glyphs for the fonts used in this style. When font stacks
187
- * are used in the style.json (e.g. lists of prefered fonts like with CSS),
188
- * then the first font in the stack is downloaded. Defaults to downloading all
189
- * UTF character ranges, which may be overkill for some styles. TODO: add more
190
- * options here.
191
- *
192
- * Returns an async generator of readable streams of glyph data and glyph info
193
- * objects.
194
- *
195
- * @param {object} opts
196
- * @param {(progress: GlyphDownloadStats) => void} [opts.onprogress]
197
- * @returns {AsyncGenerator<[import('stream').Readable, GlyphInfo]>}
198
- */
199
- async *getGlyphs({ onprogress = noop } = {}) {
200
- const style = await this.getStyle()
201
- if (!style.glyphs) return
202
-
203
- let completed = 0
204
- /** @type {GlyphDownloadStats} */
205
- let stats = {
206
- total: 0,
207
- downloaded: 0,
208
- totalBytes: 0,
209
- }
210
- /** @type {import('./utils/streams.js').ProgressCallback} */
211
- function onDownloadProgress({ chunkBytes }) {
212
- stats.totalBytes += chunkBytes
213
- onprogress(stats)
214
- }
215
- function onDownloadComplete() {
216
- stats.downloaded = ++completed
217
- onprogress(stats)
218
- }
219
-
220
- /** @type {Queue<[Promise<void | DownloadResponse>, GlyphInfo]>} */
221
- const queue = new Queue()
222
- /** @type {Map<string, string>} */
223
- const fontStacks = new Map()
224
- mapFontStacks(style.layers, (fontStack) => {
225
- // Assume that the font we get back from the API is the first font in the
226
- // font stack. TODO: When we know the API, we can check this font is
227
- // actually available.
228
- fontStacks.set(fontStack[0], fontStack.join(','))
229
- return []
230
- })
231
- const glyphUrl = normalizeGlyphsURL(style.glyphs, this.#mapboxAccessToken)
232
-
233
- for (const [font, fontStack] of fontStacks.entries()) {
234
- for (let i = 0; i < Math.pow(2, 16); i += 256) {
235
- /** @type {GlyphRange} */
236
- const range = `${i}-${i + 255}`
237
- const url = glyphUrl
238
- .replace('{fontstack}', fontStack)
239
- .replace('{range}', range)
240
- const result = this.#fetchQueue
241
- .fetch(url, { onprogress: onDownloadProgress })
242
- // TODO: Handle errors downloading glyphs
243
- .catch(noop)
244
- queue.enqueue([result, { font, range }])
245
- }
246
- }
247
-
248
- stats.total = queue.size
249
- if (onprogress) onprogress(stats)
250
-
251
- for (const [result, glyphInfo] of queue) {
252
- // TODO: Handle errors downloading glyphs
253
- const downloadResponse = await result.catch(noop)
254
- if (!downloadResponse) continue
255
- const { body } = downloadResponse
256
- // Glyphs are always gzipped. Unfortunately we can't stop fetch from ungzipping, so we need to re-gzip it.
257
- const gzipper = zlib.createGzip()
258
- // Cleanup on error, assumes byteCounter won't error.
259
- body.on('error', (err) => gzipper.destroy(err))
260
- body.on('end', onDownloadComplete)
261
- yield [body.pipe(gzipper), glyphInfo]
262
- }
263
- }
264
-
265
- /**
266
- * Get all the tiles for this style within the given bounds and zoom range.
267
- * Returns an async generator of readable streams of tile data and tile info
268
- * objects.
269
- *
270
- * The returned iterator also has a `skipped` property which is an
271
- * array of tiles which could not be downloaded, and a `stats` property which
272
- * is an object with the total number of tiles, downloaded tiles, and total
273
- * bytes downloaded.
274
- *
275
- * @param {object} opts
276
- * @param {import('./utils/geo.js').BBox} opts.bounds
277
- * @param {number} opts.maxzoom
278
- * @param {(progress: TileDownloadStats) => void} [opts.onprogress]
279
- * @param {boolean} [opts.trackErrors=false] Include errors in the returned array of skipped tiles - this has memory overhead so should only be used for debugging.
280
- * @returns {AsyncGenerator<[import('stream').Readable, TileInfo]> & { readonly skipped: Array<TileInfo & { error?: Error }>, readonly stats: TileDownloadStats }}
281
- */
282
- getTiles({ bounds, maxzoom, onprogress = noop, trackErrors = false }) {
283
- const _this = this
284
- /** @type {Array<TileInfo & { error?: Error }>} */
285
- const skipped = []
286
- /** @type {TileDownloadStats} */
287
- let stats = {
288
- total: 0,
289
- downloaded: 0,
290
- skipped: 0,
291
- totalBytes: 0,
292
- }
293
-
294
- /** @type {ReturnType<StyleDownloader['getTiles']>} */
295
- const tiles = (async function* () {
296
- const inlinedStyle = await _this.getStyle()
297
- for await (const [sourceId, source] of Object.entries(
298
- inlinedStyle.sources,
299
- )) {
300
- if (source.type !== 'raster' && source.type !== 'vector') {
301
- continue
302
- }
303
- // Baseline stats for this source, used in the `onprogress` closure
304
- // below. Sorry for the hard-to-follow code! `onprogress` can be called
305
- // after we are already reading the next source, hence the need for a
306
- // closure.
307
- const statsBaseline = { ...stats }
308
- const sourceTiles = downloadTiles({
309
- tileUrls: source.tiles,
310
- bounds,
311
- maxzoom: Math.min(maxzoom, source.maxzoom || maxzoom),
312
- minzoom: source.minzoom,
313
- sourceBounds: source.bounds,
314
- boundsBuffer: true,
315
- scheme: source.scheme,
316
- fetchQueue: _this.#fetchQueue,
317
- onprogress: (sourceStats) => {
318
- stats = addStats(statsBaseline, sourceStats)
319
- onprogress(stats)
320
- },
321
- trackErrors,
322
- })
323
- for await (const [tileDataStream, tileInfo] of sourceTiles) {
324
- yield [tileDataStream, { ...tileInfo, sourceId }]
325
- }
326
- Array.prototype.push.apply(
327
- skipped,
328
- sourceTiles.skipped.map((tile) => ({ ...tile, sourceId })),
329
- )
330
- }
331
- })()
332
-
333
- Object.defineProperty(tiles, 'skipped', {
334
- get() {
335
- return skipped
336
- },
337
- })
338
-
339
- Object.defineProperty(tiles, 'stats', {
340
- get() {
341
- return stats
342
- },
343
- })
344
-
345
- return tiles
346
- }
347
- }
348
-
349
- /**
350
- * Add two TileDownloadStats objects together.
351
- *
352
- * @param {TileDownloadStats} statsA
353
- * @param {TileDownloadStats} statsB
354
- * @returns {TileDownloadStats}
355
- */
356
- function addStats(statsA, statsB) {
357
- return {
358
- total: statsA.total + statsB.total,
359
- downloaded: statsA.downloaded + statsB.downloaded,
360
- skipped: statsA.skipped + statsB.skipped,
361
- totalBytes: statsA.totalBytes + statsB.totalBytes,
362
- }
363
- }
@@ -1,188 +0,0 @@
1
- import SphericalMercator from '@mapbox/sphericalmercator'
2
- import Queue from 'yocto-queue'
3
- import zlib from 'zlib'
4
-
5
- import { FetchQueue } from './utils/fetch.js'
6
- import {
7
- getFormatFromMimeType,
8
- getTileFormatFromStream,
9
- } from './utils/file-formats.js'
10
- import { getTileUrl, MAX_BOUNDS } from './utils/geo.js'
11
- import { noop } from './utils/misc.js'
12
-
13
- /** @typedef {Omit<import('./writer.js').TileInfo, 'sourceId'>} TileInfo */
14
- /**
15
- * @typedef {object} TileDownloadStats
16
- * @property {number} total
17
- * @property {number} downloaded
18
- * @property {number} skipped
19
- * @property {number} totalBytes
20
- */
21
-
22
- /**
23
- * Download tiles from a list of tile URLs within a bounding box and zoom range.
24
- * Returns an async generator of tile data as readable streams and tile info objects.
25
- *
26
- * @param {object} opts
27
- * @param {string[]} opts.tileUrls Array of tile URL templates. Use `{x}`, `{y}`, `{z}` placeholders, and optional `{scheme}` placeholder which can be `xyz` or `tms`.
28
- * @param {import('./utils/geo.js').BBox} opts.bounds Bounding box of the area to download
29
- * @param {number} opts.maxzoom Maximum zoom level to download
30
- * @param {(progress: TileDownloadStats) => void} [opts.onprogress] Callback to report download progress
31
- * @param {boolean} [opts.trackErrors=false] Include errors in the returned array of skipped tiles - this has memory overhead so should only be used for debugging.
32
- * @param {import('./utils/geo.js').BBox} [opts.sourceBounds=MAX_BOUNDS] Bounding box of source data.
33
- * @param {boolean} [opts.boundsBuffer=false] Buffer the bounds by one tile at each zoom level to ensure no tiles are missed at the edges. With this set to false, in most instances the map will appear incomplete when viewed because the downloaded tiles at lower zoom levels will not cover the map view area.
34
- * @param {number} [opts.minzoom=0] Minimum zoom level to download (for most cases this should be left as `0` - the size overhead is minimal, because each zoom level has 4x as many tiles)
35
- * @param {number} [opts.concurrency=8] Number of concurrent downloads (ignored if `fetchQueue` is provided)
36
- * @param {FetchQueue} [opts.fetchQueue=new FetchQueue(concurrency)] Optional fetch queue to use for downloading tiles
37
- * @param {'xyz' | 'tms'} [opts.scheme='xyz'] Tile scheme to use for tile URLs
38
- * @returns {AsyncGenerator<[import('stream').Readable, TileInfo]> & { readonly skipped: Array<TileInfo & { error?: Error }>, readonly stats: TileDownloadStats }}
39
- */
40
- export function downloadTiles({
41
- tileUrls,
42
- bounds,
43
- maxzoom,
44
- onprogress = noop,
45
- trackErrors = false,
46
- sourceBounds = MAX_BOUNDS,
47
- boundsBuffer = false,
48
- minzoom = 0,
49
- concurrency = 8,
50
- fetchQueue = new FetchQueue(concurrency),
51
- scheme = 'xyz',
52
- }) {
53
- /** @type {Array<TileInfo & { error?: Error }>} */
54
- const skipped = []
55
- let completed = 0
56
- /** @type {TileDownloadStats} */
57
- let stats = {
58
- total: 0,
59
- downloaded: 0,
60
- skipped: 0,
61
- totalBytes: 0,
62
- }
63
- /** @type {import('./utils/streams.js').ProgressCallback} */
64
- function onDownloadProgress({ chunkBytes }) {
65
- stats.totalBytes += chunkBytes
66
- onprogress(stats)
67
- }
68
- /**
69
- *
70
- * @param {Error} error
71
- * @param {TileInfo} tileInfo
72
- */
73
- function onDownloadError(error, tileInfo) {
74
- if (trackErrors) {
75
- skipped.push({ ...tileInfo, error })
76
- } else {
77
- skipped.push(tileInfo)
78
- }
79
- onprogress(stats)
80
- }
81
- function onDownloadComplete() {
82
- stats.downloaded = ++completed - skipped.length
83
- stats.skipped = skipped.length
84
- onprogress(stats)
85
- }
86
-
87
- /** @type {ReturnType<downloadTiles>} */
88
- const tiles = (async function* () {
89
- /** @type {Queue<[Promise<void | import('./utils/fetch.js').DownloadResponse>, TileInfo]>} */
90
- const queue = new Queue()
91
- const tiles = tileIterator({
92
- bounds,
93
- minzoom,
94
- maxzoom,
95
- sourceBounds,
96
- boundsBuffer,
97
- })
98
- for (const { x, y, z } of tiles) {
99
- const tileURL = getTileUrl(tileUrls, { x, y, z, scheme })
100
- const tileInfo = { z, x, y }
101
- const result = fetchQueue
102
- .fetch(tileURL, { onprogress: onDownloadProgress })
103
- // We handle error here rather than below to avoid uncaught errors
104
- .catch((err) => onDownloadError(err, tileInfo))
105
- queue.enqueue([result, tileInfo])
106
- }
107
-
108
- stats.total = queue.size
109
- if (onprogress) onprogress(stats)
110
-
111
- for (const [result, tileInfo] of queue) {
112
- // We handle any error above and add to `skipped`
113
- const downloadResponse = await result.catch(noop)
114
- if (!downloadResponse) continue
115
- let { body, mimeType } = downloadResponse
116
- body.on('end', onDownloadComplete)
117
- body.on('error', (err) => onDownloadError(err, tileInfo))
118
- /** @type {import('./writer.js').TileFormat} */
119
- let format
120
- if (mimeType) {
121
- format = getFormatFromMimeType(mimeType)
122
- } else {
123
- ;[format, body] = await getTileFormatFromStream(body)
124
- }
125
-
126
- let stream = body
127
- if (format === 'mvt') {
128
- // MVT tiles are always gzipped. Unfortunately we can't stop fetch from
129
- // ungzipping the data during download, so we need to re-gzip it.
130
- const gzipStream = zlib.createGzip()
131
- stream = body.pipe(gzipStream)
132
- stream.on('error', (err) => gzipStream.destroy(err))
133
- }
134
-
135
- yield [stream, { ...tileInfo, format }]
136
- }
137
- })()
138
-
139
- Object.defineProperty(tiles, 'skipped', {
140
- get() {
141
- return skipped
142
- },
143
- })
144
-
145
- Object.defineProperty(tiles, 'stats', {
146
- get() {
147
- return stats
148
- },
149
- })
150
-
151
- return tiles
152
- }
153
-
154
- /**
155
- *
156
- * @param {object} opts
157
- * @param {import('./utils/geo.js').BBox} [opts.bounds]
158
- * @param {import('./utils/geo.js').BBox} [opts.sourceBounds]
159
- * @param {boolean} [opts.boundsBuffer]
160
- * @param {number} [opts.minzoom]
161
- * @param {number} opts.maxzoom
162
- */
163
- export function* tileIterator({
164
- bounds = [...MAX_BOUNDS],
165
- minzoom = 0,
166
- maxzoom,
167
- sourceBounds,
168
- boundsBuffer = false,
169
- }) {
170
- const sm = new SphericalMercator({ size: 256 })
171
- for (let z = minzoom; z <= maxzoom; z++) {
172
- // Cloning bounds passed to sm.xyz because no guarantee it won't mutate the array
173
- let { minX, minY, maxX, maxY } = sm.xyz([...bounds], z)
174
- let sourceXYBounds = sourceBounds
175
- ? sm.xyz([...sourceBounds], z)
176
- : { minX, minY, maxX, maxY }
177
- const buffer = boundsBuffer ? 1 : 0
178
- minX = Math.max(0, minX - buffer, sourceXYBounds.minX)
179
- minY = Math.max(0, minY - buffer, sourceXYBounds.minY)
180
- maxX = Math.min(Math.pow(2, z) - 1, maxX + buffer, sourceXYBounds.maxX)
181
- maxY = Math.min(Math.pow(2, z) - 1, maxY + buffer, sourceXYBounds.maxY)
182
- for (let x = minX; x <= maxX; x++) {
183
- for (let y = minY; y <= maxY; y++) {
184
- yield { x, y, z }
185
- }
186
- }
187
- }
188
- }