styled-map-package-api 5.0.0-pre.3 → 5.0.0-pre.4

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 (94) hide show
  1. package/README.md +94 -0
  2. package/dist/download.d.ts +11 -21
  3. package/dist/fallbacks.d.ts +32 -0
  4. package/dist/from-mbtiles.d.ts +1 -3
  5. package/dist/index.d.ts +11 -24
  6. package/dist/reader.d.ts +28 -12
  7. package/dist/server.d.ts +23 -14
  8. package/dist/style-downloader.d.ts +13 -19
  9. package/dist/tile-downloader.d.ts +13 -23
  10. package/dist/types.d.ts +61 -0
  11. package/dist/utils/errors.d.ts +2 -4
  12. package/dist/utils/fetch.d.ts +3 -8
  13. package/dist/utils/file-formats.d.ts +3 -10
  14. package/dist/utils/geo.d.ts +17 -9
  15. package/dist/utils/mapbox.d.ts +8 -10
  16. package/dist/utils/misc.d.ts +3 -5
  17. package/dist/utils/streams.d.ts +6 -10
  18. package/dist/utils/style.d.ts +27 -16
  19. package/dist/utils/templates.d.ts +30 -25
  20. package/dist/validator.d.ts +66 -0
  21. package/dist/writer.d.ts +157 -4
  22. package/lib/download.js +125 -0
  23. package/lib/fallbacks.js +157 -0
  24. package/lib/from-mbtiles.js +131 -0
  25. package/lib/index.js +12 -0
  26. package/lib/reader.js +360 -0
  27. package/lib/server.js +222 -0
  28. package/lib/style-downloader.js +369 -0
  29. package/lib/tile-downloader.js +189 -0
  30. package/lib/types.ts +99 -0
  31. package/lib/utils/errors.js +24 -0
  32. package/lib/utils/fetch.js +104 -0
  33. package/lib/utils/file-formats.js +92 -0
  34. package/lib/utils/geo.js +97 -0
  35. package/lib/utils/mapbox.js +155 -0
  36. package/{dist/utils/misc.d.cts → lib/utils/misc.js} +9 -5
  37. package/lib/utils/streams.js +101 -0
  38. package/lib/utils/style.js +206 -0
  39. package/lib/utils/templates.js +165 -0
  40. package/lib/validator.js +789 -0
  41. package/lib/writer.js +652 -0
  42. package/package.json +30 -78
  43. package/dist/download.cjs +0 -100
  44. package/dist/download.d.cts +0 -63
  45. package/dist/download.js +0 -76
  46. package/dist/from-mbtiles.cjs +0 -127
  47. package/dist/from-mbtiles.d.cts +0 -14
  48. package/dist/from-mbtiles.js +0 -103
  49. package/dist/index.cjs +0 -46
  50. package/dist/index.d.cts +0 -24
  51. package/dist/index.js +0 -16
  52. package/dist/reader.cjs +0 -287
  53. package/dist/reader.d.cts +0 -67
  54. package/dist/reader.js +0 -259
  55. package/dist/server.cjs +0 -73
  56. package/dist/server.d.cts +0 -45
  57. package/dist/server.js +0 -49
  58. package/dist/style-downloader.cjs +0 -314
  59. package/dist/style-downloader.d.cts +0 -118
  60. package/dist/style-downloader.js +0 -290
  61. package/dist/tile-downloader.cjs +0 -156
  62. package/dist/tile-downloader.d.cts +0 -82
  63. package/dist/tile-downloader.js +0 -124
  64. package/dist/types-Bhn0-Ldk.d.cts +0 -201
  65. package/dist/types-Bhn0-Ldk.d.ts +0 -201
  66. package/dist/utils/errors.cjs +0 -41
  67. package/dist/utils/errors.d.cts +0 -18
  68. package/dist/utils/errors.js +0 -16
  69. package/dist/utils/fetch.cjs +0 -97
  70. package/dist/utils/fetch.d.cts +0 -50
  71. package/dist/utils/fetch.js +0 -63
  72. package/dist/utils/file-formats.cjs +0 -96
  73. package/dist/utils/file-formats.d.cts +0 -32
  74. package/dist/utils/file-formats.js +0 -70
  75. package/dist/utils/geo.cjs +0 -84
  76. package/dist/utils/geo.d.cts +0 -46
  77. package/dist/utils/geo.js +0 -56
  78. package/dist/utils/mapbox.cjs +0 -121
  79. package/dist/utils/mapbox.d.cts +0 -43
  80. package/dist/utils/mapbox.js +0 -91
  81. package/dist/utils/misc.cjs +0 -39
  82. package/dist/utils/misc.js +0 -13
  83. package/dist/utils/streams.cjs +0 -99
  84. package/dist/utils/streams.d.cts +0 -49
  85. package/dist/utils/streams.js +0 -73
  86. package/dist/utils/style.cjs +0 -126
  87. package/dist/utils/style.d.cts +0 -66
  88. package/dist/utils/style.js +0 -98
  89. package/dist/utils/templates.cjs +0 -124
  90. package/dist/utils/templates.d.cts +0 -79
  91. package/dist/utils/templates.js +0 -85
  92. package/dist/writer.cjs +0 -539
  93. package/dist/writer.d.cts +0 -4
  94. package/dist/writer.js +0 -516
@@ -0,0 +1,369 @@
1
+ import { migrate } from '@maplibre/maplibre-gl-style-spec'
2
+ import { check as checkGeoJson } from '@placemarkio/check-geojson'
3
+ import { includeKeys } from 'filter-obj'
4
+ import ky from 'ky'
5
+ import Queue from 'yocto-queue'
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
+ isLocallyRenderedRange,
20
+ mapFontStacks,
21
+ validateStyle,
22
+ } from './utils/style.js'
23
+
24
+ /** @import { SourceSpecification, StyleSpecification } from '@maplibre/maplibre-gl-style-spec' */
25
+ /** @import { TileInfo, GlyphInfo, GlyphRange } from './writer.js' */
26
+ /** @import { TileDownloadStats } from './tile-downloader.js' */
27
+ /** @import { StyleInlinedSources, InlinedSource } from './types.js'*/
28
+
29
+ /** @typedef { import('ky').ResponsePromise & { body: ReadableStream<Uint8Array> } } ResponsePromise */
30
+ /** @import { DownloadResponse } from './utils/fetch.js' */
31
+
32
+ /**
33
+ * @typedef {object} GlyphDownloadStats
34
+ * @property {number} total
35
+ * @property {number} downloaded
36
+ * @property {number} totalBytes
37
+ */
38
+
39
+ /**
40
+ * Download a style and its resources for offline use. Please check the terms of
41
+ * service of the map provider you are using before downloading any resources.
42
+ */
43
+ export class StyleDownloader {
44
+ /** @type {null | string} */
45
+ #styleURL = null
46
+ /** @type {null | StyleSpecification} */
47
+ #inputStyle = null
48
+ /** @type {FetchQueue} */
49
+ #fetchQueue
50
+ #mapboxAccessToken
51
+
52
+ /**
53
+ * @param {string | StyleSpecification} style A url to a style JSON file or a style object
54
+ * @param {object} [opts]
55
+ * @param {number} [opts.concurrency=8]
56
+ * @param {string} [opts.mapboxAccessToken] Downloading a style from Mapbox requires an access token
57
+ */
58
+ constructor(style, { concurrency = 8, mapboxAccessToken } = {}) {
59
+ if (typeof style === 'string') {
60
+ const { searchParams } = new URL(style)
61
+ this.#mapboxAccessToken =
62
+ searchParams.get('access_token') || mapboxAccessToken
63
+ this.#styleURL = normalizeStyleURL(style, this.#mapboxAccessToken)
64
+ } else {
65
+ // Migrate actually mutates the input, so we act on a clone.
66
+ const styleV8 = migrate(clone(style))
67
+ if (!validateStyle(styleV8)) {
68
+ throw new AggregateError(validateStyle.errors, 'Invalid style')
69
+ }
70
+ this.#inputStyle = styleV8
71
+ }
72
+ this.#fetchQueue = new FetchQueue(concurrency)
73
+ }
74
+
75
+ /**
76
+ * Number of active downloads.
77
+ */
78
+ get active() {
79
+ return this.#fetchQueue.activeCount
80
+ }
81
+
82
+ /**
83
+ * Download the style JSON for this style and inline the sources
84
+ *
85
+ * @returns {Promise<StyleInlinedSources>}
86
+ */
87
+ async getStyle() {
88
+ if (!this.#inputStyle && this.#styleURL) {
89
+ const downloadedStyle = await ky(this.#styleURL).json()
90
+ const styleV8 = migrate(downloadedStyle)
91
+ if (!validateStyle(styleV8)) {
92
+ throw new AggregateError(validateStyle.errors, 'Invalid style')
93
+ }
94
+ this.#inputStyle = styleV8
95
+ } else if (!this.#inputStyle) {
96
+ throw new Error('Unexpected state: no style or style URL provided')
97
+ }
98
+ /** @type {{ [_:string]: InlinedSource }} */
99
+ const inlinedSources = {}
100
+ for (const [sourceId, source] of Object.entries(this.#inputStyle.sources)) {
101
+ inlinedSources[sourceId] = await this.#getInlinedSource(source)
102
+ }
103
+ return {
104
+ ...this.#inputStyle,
105
+ sources: inlinedSources,
106
+ }
107
+ }
108
+
109
+ /**
110
+ * @param {SourceSpecification} source
111
+ * @returns {Promise<InlinedSource>}
112
+ */
113
+ async #getInlinedSource(source) {
114
+ if (isInlinedSource(source)) {
115
+ return source
116
+ }
117
+ if (
118
+ source.type === 'raster' ||
119
+ source.type === 'vector' ||
120
+ source.type === 'raster-dem'
121
+ ) {
122
+ if (!source.url) {
123
+ throw new Error('Source is missing both url and tiles properties')
124
+ }
125
+ const sourceUrl = normalizeSourceURL(source.url, this.#mapboxAccessToken)
126
+ const tilejson = await ky(sourceUrl).json()
127
+ assertTileJSON(tilejson)
128
+ return {
129
+ ...source,
130
+ ...includeKeys(tilejson, [
131
+ 'bounds',
132
+ 'maxzoom',
133
+ 'minzoom',
134
+ 'tiles',
135
+ 'description',
136
+ 'attribution',
137
+ 'vector_layers',
138
+ ]),
139
+ }
140
+ } else if (source.type === 'geojson') {
141
+ if (typeof source.data !== 'string') {
142
+ // Shouldn't get here because of the `isInlineSource()` check above, but
143
+ // Typescript can't fiture that out.
144
+ throw new Error('Unexpected data property for GeoJson source')
145
+ }
146
+ const geojsonUrl = normalizeSourceURL(
147
+ source.data,
148
+ this.#mapboxAccessToken,
149
+ )
150
+ const data = checkGeoJson(await ky(geojsonUrl).text())
151
+ return {
152
+ ...source,
153
+ data,
154
+ }
155
+ }
156
+ return source
157
+ }
158
+
159
+ /**
160
+ * Download the sprite PNGs and JSON files for this style. Returns an async
161
+ * generator of json and png readable streams, and the sprite id and pixel
162
+ * ratio. Downloads pixel ratios `1` and `2`.
163
+ *
164
+ * @returns {AsyncGenerator<{ json: ReadableStream<Uint8Array>, png: ReadableStream<Uint8Array>, id: string, pixelRatio: number }>}
165
+ */
166
+ async *getSprites() {
167
+ const style = await this.getStyle()
168
+ if (!style.sprite) return
169
+ const accessToken = this.#mapboxAccessToken
170
+ const spriteDefs = Array.isArray(style.sprite)
171
+ ? style.sprite
172
+ : [{ id: 'default', url: style.sprite }]
173
+ for (const { id, url } of spriteDefs) {
174
+ for (const pixelRatio of [1, 2]) {
175
+ const format = pixelRatio === 1 ? '' : '@2x'
176
+ const jsonUrl = normalizeSpriteURL(url, format, '.json', accessToken)
177
+ const pngUrl = normalizeSpriteURL(url, format, '.png', accessToken)
178
+ const [{ body: json }, { body: png }] = await Promise.all([
179
+ this.#fetchQueue.fetch(jsonUrl),
180
+ this.#fetchQueue.fetch(pngUrl),
181
+ ])
182
+ yield { json, png, id, pixelRatio }
183
+ }
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Download all the glyphs for the fonts used in this style. When font stacks
189
+ * are used in the style.json (e.g. lists of prefered fonts like with CSS),
190
+ * then the first font in the stack is downloaded. Defaults to downloading all
191
+ * UTF character ranges, which may be overkill for some styles. TODO: add more
192
+ * options here.
193
+ *
194
+ * Returns an async generator of readable streams of glyph data and glyph info
195
+ * objects.
196
+ *
197
+ * @param {object} opts
198
+ * @param {(progress: GlyphDownloadStats) => void} [opts.onprogress]
199
+ * @param {boolean} [opts.skipLocalGlyphs] Skip glyph ranges rendered client-side by MapLibre GL via localIdeographFontFamily (CJK, Hangul, Kana, Yi, etc.)
200
+ * @returns {AsyncGenerator<[ReadableStream<Uint8Array>, GlyphInfo]>}
201
+ */
202
+ async *getGlyphs({ onprogress = noop, skipLocalGlyphs = false } = {}) {
203
+ const style = await this.getStyle()
204
+ if (!style.glyphs) return
205
+
206
+ let completed = 0
207
+ /** @type {GlyphDownloadStats} */
208
+ let stats = {
209
+ total: 0,
210
+ downloaded: 0,
211
+ totalBytes: 0,
212
+ }
213
+ /** @type {import('./utils/streams.js').ProgressCallback} */
214
+ function onDownloadProgress({ chunkBytes }) {
215
+ stats.totalBytes += chunkBytes
216
+ onprogress(stats)
217
+ }
218
+ function onDownloadComplete() {
219
+ stats.downloaded = ++completed
220
+ onprogress(stats)
221
+ }
222
+
223
+ /** @type {Queue<[Promise<void | DownloadResponse>, GlyphInfo]>} */
224
+ const queue = new Queue()
225
+ /** @type {Map<string, string>} */
226
+ const fontStacks = new Map()
227
+ mapFontStacks(style.layers, (fontStack) => {
228
+ // Assume that the font we get back from the API is the first font in the
229
+ // font stack. TODO: When we know the API, we can check this font is
230
+ // actually available.
231
+ fontStacks.set(fontStack[0], fontStack.join(','))
232
+ return []
233
+ })
234
+ const glyphUrl = normalizeGlyphsURL(style.glyphs, this.#mapboxAccessToken)
235
+
236
+ for (const [font, fontStack] of fontStacks.entries()) {
237
+ for (let i = 0; i < Math.pow(2, 16); i += 256) {
238
+ if (skipLocalGlyphs && isLocallyRenderedRange(i)) continue
239
+ /** @type {GlyphRange} */
240
+ const range = `${i}-${i + 255}`
241
+ const url = glyphUrl
242
+ .replace('{fontstack}', fontStack)
243
+ .replace('{range}', range)
244
+ const result = this.#fetchQueue
245
+ .fetch(url, { onprogress: onDownloadProgress })
246
+ // TODO: Handle errors downloading glyphs
247
+ .catch(noop)
248
+ queue.enqueue([result, { font, range }])
249
+ }
250
+ }
251
+
252
+ stats.total = queue.size
253
+ if (onprogress) onprogress(stats)
254
+
255
+ for (const [result, glyphInfo] of queue) {
256
+ // TODO: Handle errors downloading glyphs
257
+ const downloadResponse = await result.catch(noop)
258
+ if (!downloadResponse) continue
259
+ const { body } = downloadResponse
260
+ // Glyphs are always gzipped. Unfortunately we can't stop fetch from ungzipping, so we need to re-gzip it.
261
+ // Pipe body directly into the CompressionStream so pipeTo's resolved promise signals when consumer is done.
262
+ const gzip = /** @type {TransformStream<Uint8Array, Uint8Array>} */ (
263
+ new CompressionStream('gzip')
264
+ )
265
+ body.pipeTo(gzip.writable).then(onDownloadComplete, noop)
266
+ const gzippedStream = gzip.readable
267
+ yield [gzippedStream, glyphInfo]
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Get all the tiles for this style within the given bounds and zoom range.
273
+ * Returns an async generator of readable streams of tile data and tile info
274
+ * objects.
275
+ *
276
+ * The returned iterator also has a `skipped` property which is an
277
+ * array of tiles which could not be downloaded, and a `stats` property which
278
+ * is an object with the total number of tiles, downloaded tiles, and total
279
+ * bytes downloaded.
280
+ *
281
+ * @param {object} opts
282
+ * @param {Readonly<import('./utils/geo.js').BBox>} opts.bounds
283
+ * @param {number} opts.maxzoom
284
+ * @param {(progress: TileDownloadStats) => void} [opts.onprogress]
285
+ * @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.
286
+ * @returns {AsyncGenerator<[ReadableStream<Uint8Array>, TileInfo]> & { readonly skipped: Array<TileInfo & { error?: Error }>, readonly stats: TileDownloadStats }}
287
+ */
288
+ getTiles({ bounds, maxzoom, onprogress = noop, trackErrors = false }) {
289
+ const _this = this
290
+ /** @type {Array<TileInfo & { error?: Error }>} */
291
+ const skipped = []
292
+ /** @type {TileDownloadStats} */
293
+ let stats = {
294
+ total: 0,
295
+ downloaded: 0,
296
+ skipped: 0,
297
+ totalBytes: 0,
298
+ }
299
+
300
+ /** @type {ReturnType<StyleDownloader['getTiles']>} */
301
+ const tiles = (async function* () {
302
+ const inlinedStyle = await _this.getStyle()
303
+ for await (const [sourceId, source] of Object.entries(
304
+ inlinedStyle.sources,
305
+ )) {
306
+ if (source.type !== 'raster' && source.type !== 'vector') {
307
+ continue
308
+ }
309
+ // Baseline stats for this source, used in the `onprogress` closure
310
+ // below. Sorry for the hard-to-follow code! `onprogress` can be called
311
+ // after we are already reading the next source, hence the need for a
312
+ // closure.
313
+ const statsBaseline = { ...stats }
314
+ const sourceTiles = downloadTiles({
315
+ tileUrls: source.tiles,
316
+ bounds,
317
+ maxzoom: Math.min(maxzoom, source.maxzoom || maxzoom),
318
+ minzoom: source.minzoom,
319
+ sourceBounds: source.bounds,
320
+ boundsBuffer: true,
321
+ scheme: source.scheme,
322
+ fetchQueue: _this.#fetchQueue,
323
+ onprogress: (sourceStats) => {
324
+ stats = addStats(statsBaseline, sourceStats)
325
+ onprogress(stats)
326
+ },
327
+ trackErrors,
328
+ })
329
+ for await (const [tileDataStream, tileInfo] of sourceTiles) {
330
+ yield [tileDataStream, { ...tileInfo, sourceId }]
331
+ }
332
+ Array.prototype.push.apply(
333
+ skipped,
334
+ sourceTiles.skipped.map((tile) => ({ ...tile, sourceId })),
335
+ )
336
+ }
337
+ })()
338
+
339
+ Object.defineProperty(tiles, 'skipped', {
340
+ get() {
341
+ return skipped
342
+ },
343
+ })
344
+
345
+ Object.defineProperty(tiles, 'stats', {
346
+ get() {
347
+ return stats
348
+ },
349
+ })
350
+
351
+ return tiles
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Add two TileDownloadStats objects together.
357
+ *
358
+ * @param {TileDownloadStats} statsA
359
+ * @param {TileDownloadStats} statsB
360
+ * @returns {TileDownloadStats}
361
+ */
362
+ function addStats(statsA, statsB) {
363
+ return {
364
+ total: statsA.total + statsB.total,
365
+ downloaded: statsA.downloaded + statsB.downloaded,
366
+ skipped: statsA.skipped + statsB.skipped,
367
+ totalBytes: statsA.totalBytes + statsB.totalBytes,
368
+ }
369
+ }
@@ -0,0 +1,189 @@
1
+ import SphericalMercator from '@mapbox/sphericalmercator'
2
+ import Queue from 'yocto-queue'
3
+
4
+ import { FetchQueue } from './utils/fetch.js'
5
+ import {
6
+ getFormatFromMimeType,
7
+ getTileFormatFromStream,
8
+ } from './utils/file-formats.js'
9
+ import { getTileUrl, MAX_BOUNDS } from './utils/geo.js'
10
+ import { noop } from './utils/misc.js'
11
+
12
+ /** @typedef {Omit<import('./writer.js').TileInfo, 'sourceId'>} TileInfo */
13
+ /**
14
+ * @typedef {object} TileDownloadStats
15
+ * @property {number} total
16
+ * @property {number} downloaded
17
+ * @property {number} skipped
18
+ * @property {number} totalBytes
19
+ */
20
+
21
+ /**
22
+ * Download tiles from a list of tile URLs within a bounding box and zoom range.
23
+ * Returns an async generator of tile data as readable streams and tile info objects.
24
+ *
25
+ * @param {object} opts
26
+ * @param {string[]} opts.tileUrls Array of tile URL templates. Use `{x}`, `{y}`, `{z}` placeholders, and optional `{scheme}` placeholder which can be `xyz` or `tms`.
27
+ * @param {Readonly<import('./utils/geo.js').BBox>} opts.bounds Bounding box of the area to download
28
+ * @param {number} opts.maxzoom Maximum zoom level to download
29
+ * @param {(progress: TileDownloadStats) => void} [opts.onprogress] Callback to report download progress
30
+ * @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.
31
+ * @param {Readonly<import('./utils/geo.js').BBox>} [opts.sourceBounds=MAX_BOUNDS] Bounding box of source data.
32
+ * @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.
33
+ * @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)
34
+ * @param {number} [opts.concurrency=8] Number of concurrent downloads (ignored if `fetchQueue` is provided)
35
+ * @param {FetchQueue} [opts.fetchQueue=new FetchQueue(concurrency)] Optional fetch queue to use for downloading tiles
36
+ * @param {'xyz' | 'tms'} [opts.scheme='xyz'] Tile scheme to use for tile URLs
37
+ * @returns {AsyncGenerator<[ReadableStream<Uint8Array>, TileInfo]> & { readonly skipped: Array<TileInfo & { error?: Error }>, readonly stats: TileDownloadStats }}
38
+ */
39
+ export function downloadTiles({
40
+ tileUrls,
41
+ bounds,
42
+ maxzoom,
43
+ onprogress = noop,
44
+ trackErrors = false,
45
+ sourceBounds = MAX_BOUNDS,
46
+ boundsBuffer = false,
47
+ minzoom = 0,
48
+ concurrency = 8,
49
+ fetchQueue = new FetchQueue(concurrency),
50
+ scheme = 'xyz',
51
+ }) {
52
+ /** @type {Array<TileInfo & { error?: Error }>} */
53
+ const skipped = []
54
+ let completed = 0
55
+ /** @type {TileDownloadStats} */
56
+ let stats = {
57
+ total: 0,
58
+ downloaded: 0,
59
+ skipped: 0,
60
+ totalBytes: 0,
61
+ }
62
+ /** @type {import('./utils/streams.js').ProgressCallback} */
63
+ function onDownloadProgress({ chunkBytes }) {
64
+ stats.totalBytes += chunkBytes
65
+ onprogress(stats)
66
+ }
67
+ /**
68
+ *
69
+ * @param {Error} error
70
+ * @param {TileInfo} tileInfo
71
+ */
72
+ function onDownloadError(error, tileInfo) {
73
+ if (trackErrors) {
74
+ skipped.push({ ...tileInfo, error })
75
+ } else {
76
+ skipped.push(tileInfo)
77
+ }
78
+ onprogress(stats)
79
+ }
80
+ function onDownloadComplete() {
81
+ stats.downloaded = ++completed - skipped.length
82
+ stats.skipped = skipped.length
83
+ onprogress(stats)
84
+ }
85
+
86
+ /** @type {ReturnType<downloadTiles>} */
87
+ const tiles = (async function* () {
88
+ /** @type {Queue<[Promise<void | import('./utils/fetch.js').DownloadResponse>, TileInfo]>} */
89
+ const queue = new Queue()
90
+ const tiles = tileIterator({
91
+ bounds,
92
+ minzoom,
93
+ maxzoom,
94
+ sourceBounds,
95
+ boundsBuffer,
96
+ })
97
+ for (const { x, y, z } of tiles) {
98
+ const tileURL = getTileUrl(tileUrls, { x, y, z, scheme })
99
+ const tileInfo = { z, x, y }
100
+ const result = fetchQueue
101
+ .fetch(tileURL, { onprogress: onDownloadProgress })
102
+ // We handle error here rather than below to avoid uncaught errors
103
+ .catch((err) => onDownloadError(err, tileInfo))
104
+ queue.enqueue([result, tileInfo])
105
+ }
106
+
107
+ stats.total = queue.size
108
+ if (onprogress) onprogress(stats)
109
+
110
+ for (const [result, tileInfo] of queue) {
111
+ // We handle any error above and add to `skipped`
112
+ const downloadResponse = await result.catch(noop)
113
+ if (!downloadResponse) continue
114
+ let { body, mimeType } = downloadResponse
115
+ /** @type {import('./writer.js').TileFormat} */
116
+ let format
117
+ if (mimeType) {
118
+ format = getFormatFromMimeType(mimeType)
119
+ } else {
120
+ ;[format, body] = await getTileFormatFromStream(body)
121
+ }
122
+
123
+ let stream = body
124
+ // MVT tiles are always gzipped. Unfortunately we can't stop fetch from
125
+ // ungzipping the data during download, so we need to re-gzip it.
126
+ // Use the gzip transform (or a passthrough for other formats) as the pipe
127
+ // target so pipeTo's resolved promise signals when the consumer is done.
128
+ const transform = /** @type {TransformStream<Uint8Array, Uint8Array>} */ (
129
+ format === 'mvt' ? new CompressionStream('gzip') : new TransformStream()
130
+ )
131
+ body
132
+ .pipeTo(transform.writable)
133
+ .then(onDownloadComplete, (err) => onDownloadError(err, tileInfo))
134
+ stream = transform.readable
135
+
136
+ yield [stream, { ...tileInfo, format }]
137
+ }
138
+ })()
139
+
140
+ Object.defineProperty(tiles, 'skipped', {
141
+ get() {
142
+ return skipped
143
+ },
144
+ })
145
+
146
+ Object.defineProperty(tiles, 'stats', {
147
+ get() {
148
+ return stats
149
+ },
150
+ })
151
+
152
+ return tiles
153
+ }
154
+
155
+ /**
156
+ *
157
+ * @param {object} opts
158
+ * @param {Readonly<import('./utils/geo.js').BBox>} [opts.bounds]
159
+ * @param {Readonly<import('./utils/geo.js').BBox>} [opts.sourceBounds]
160
+ * @param {boolean} [opts.boundsBuffer]
161
+ * @param {number} [opts.minzoom]
162
+ * @param {number} opts.maxzoom
163
+ */
164
+ export function* tileIterator({
165
+ bounds = [...MAX_BOUNDS],
166
+ minzoom = 0,
167
+ maxzoom,
168
+ sourceBounds,
169
+ boundsBuffer = false,
170
+ }) {
171
+ const sm = new SphericalMercator({ size: 256 })
172
+ for (let z = minzoom; z <= maxzoom; z++) {
173
+ // Cloning bounds passed to sm.xyz because no guarantee it won't mutate the array
174
+ let { minX, minY, maxX, maxY } = sm.xyz([...bounds], z)
175
+ let sourceXYBounds = sourceBounds
176
+ ? sm.xyz([...sourceBounds], z)
177
+ : { minX, minY, maxX, maxY }
178
+ const buffer = boundsBuffer ? 1 : 0
179
+ minX = Math.max(0, minX - buffer, sourceXYBounds.minX)
180
+ minY = Math.max(0, minY - buffer, sourceXYBounds.minY)
181
+ maxX = Math.min(Math.pow(2, z) - 1, maxX + buffer, sourceXYBounds.maxX)
182
+ maxY = Math.min(Math.pow(2, z) - 1, maxY + buffer, sourceXYBounds.maxY)
183
+ for (let x = minX; x <= maxX; x++) {
184
+ for (let y = minY; y <= maxY; y++) {
185
+ yield { x, y, z }
186
+ }
187
+ }
188
+ }
189
+ }
package/lib/types.ts ADDED
@@ -0,0 +1,99 @@
1
+ import type {
2
+ SourceSpecification,
3
+ StyleSpecification,
4
+ ValidationError,
5
+ GeoJSONSourceSpecification,
6
+ VectorSourceSpecification,
7
+ RasterSourceSpecification,
8
+ RasterDEMSourceSpecification,
9
+ } from '@maplibre/maplibre-gl-style-spec'
10
+ import type { GeoJSON, BBox } from 'geojson'
11
+ import type { Except, SetRequired, Simplify } from 'type-fest'
12
+
13
+ import { SUPPORTED_SOURCE_TYPES } from './writer.js'
14
+
15
+ export type InputSource = Extract<
16
+ SourceSpecification,
17
+ { type: (typeof SUPPORTED_SOURCE_TYPES)[number] }
18
+ >
19
+ type TransformInlinedSource<T extends SourceSpecification> =
20
+ T extends GeoJSONSourceSpecification
21
+ ? OmitUnion<T, 'data'> & { data: GeoJSON }
22
+ : T extends
23
+ | VectorSourceSpecification
24
+ | RasterSourceSpecification
25
+ | RasterDEMSourceSpecification
26
+ ? SetRequired<OmitUnion<T, 'url'>, 'tiles'>
27
+ : T
28
+ /**
29
+ * This is a slightly stricter version of SourceSpecification that requires
30
+ * sources to be inlined (e.g. no urls to TileJSON or GeoJSON files).
31
+ */
32
+ export type InlinedSource = TransformInlinedSource<SourceSpecification>
33
+ type SupportedInlinedSource = Extract<
34
+ InlinedSource,
35
+ { type: (typeof SUPPORTED_SOURCE_TYPES)[number] }
36
+ >
37
+ /**
38
+ * This is a slightly stricter version of StyleSpecification that requires
39
+ * sources to be inlined (e.g. no urls to TileJSON or GeoJSON files).
40
+ */
41
+ export type StyleInlinedSources = Omit<StyleSpecification, 'sources'> & {
42
+ sources: {
43
+ [_: string]: InlinedSource
44
+ }
45
+ }
46
+
47
+ export type SMPSource = TransformSMPInputSource<SupportedInlinedSource>
48
+ /**
49
+ * This is a slightly stricter version of StyleSpecification that is provided in
50
+ * a Styled Map Package. Tile sources must have tile URLs inlined (they cannot
51
+ * refer to a TileJSON url), and they must have bounds, minzoom, and maxzoom.
52
+ * GeoJSON sources must have inlined GeoJSON (not a URL to a GeoJSON file).
53
+ */
54
+ export type SMPStyle = TransformSMPStyle<StyleSpecification>
55
+
56
+ export type TransformSMPInputSource<T extends SupportedInlinedSource> =
57
+ T extends GeoJSONSourceSpecification
58
+ ? T & { data: { bbox: BBox } }
59
+ : T extends RasterSourceSpecification | VectorSourceSpecification
60
+ ? SetRequired<T, 'bounds' | 'minzoom' | 'maxzoom'>
61
+ : T
62
+
63
+ type TransformSMPStyle<T extends StyleSpecification> = Omit<T, 'sources'> & {
64
+ metadata: {
65
+ 'smp:bounds': [number, number, number, number]
66
+ 'smp:maxzoom': 0
67
+ 'smp:sourceFolders': { [_: string]: string }
68
+ [key: string]: unknown
69
+ }
70
+ sources: {
71
+ [_: string]: SMPSource
72
+ }
73
+ }
74
+
75
+ export interface ValidateStyle {
76
+ (style: unknown): style is StyleSpecification
77
+ errors: Array<ValidationError>
78
+ }
79
+
80
+ export type DownloadStream = ReadableStream<Uint8Array>
81
+
82
+ export type RequiredUnion<T> = T extends any ? Required<T> : never
83
+ export type OmitUnion<T, K extends keyof any> = T extends unknown
84
+ ? Omit<T, K>
85
+ : never
86
+
87
+ type SetRequiredIfPresent<
88
+ BaseType,
89
+ Keys extends keyof any,
90
+ > = BaseType extends unknown
91
+ ? Keys extends keyof BaseType
92
+ ? Simplify<
93
+ // Pick just the keys that are optional from the base type.
94
+ Except<BaseType, Keys> &
95
+ // Pick the keys that should be required from the base type and make them required.
96
+ Required<Pick<BaseType, Keys>>
97
+ >
98
+ : never
99
+ : never
@@ -0,0 +1,24 @@
1
+ export class ENOENT extends Error {
2
+ code = 'ENOENT'
3
+ /** @param {string} path */
4
+ constructor(path) {
5
+ const message = `ENOENT: no such file or directory, open '${path}'`
6
+ super(message)
7
+ this.path = path
8
+ }
9
+ }
10
+
11
+ /**
12
+ * Returns true if the error if because a file is not found. On Windows, some
13
+ * operations like fs.watch() throw an EPERM error rather than ENOENT.
14
+ *
15
+ * @param {unknown} error
16
+ * @returns {error is Error & { code: 'ENOENT' | 'EPERM' }}
17
+ */
18
+ export function isFileNotThereError(error) {
19
+ return (
20
+ error instanceof Error &&
21
+ 'code' in error &&
22
+ (error.code === 'ENOENT' || error.code === 'EPERM')
23
+ )
24
+ }