styled-map-package 2.2.1 → 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/writer.js DELETED
@@ -1,478 +0,0 @@
1
- import { validateStyleMin, migrate } from '@maplibre/maplibre-gl-style-spec'
2
- import { bbox } from '@turf/bbox'
3
- import archiver from 'archiver'
4
- import { EventEmitter } from 'events'
5
- import { excludeKeys } from 'filter-obj'
6
- import fs from 'fs'
7
- import { pEvent } from 'p-event'
8
- import { PassThrough, pipeline } from 'readable-stream'
9
- import { Readable } from 'stream'
10
-
11
- import { getTileFormatFromStream } from './utils/file-formats.js'
12
- import { MAX_BOUNDS, tileToBBox, unionBBox } from './utils/geo.js'
13
- import { clone } from './utils/misc.js'
14
- import { writeStreamFromAsync } from './utils/streams.js'
15
- import { replaceFontStacks } from './utils/style.js'
16
- import {
17
- getGlyphFilename,
18
- getSpriteFilename,
19
- getSpriteUri,
20
- getTileFilename,
21
- getTileUri,
22
- GLYPH_URI,
23
- STYLE_FILE,
24
- } from './utils/templates.js'
25
-
26
- /** @typedef {string | Buffer | Uint8Array | import('stream').Readable } Source */
27
- /** @typedef {string | Buffer | import('stream').Readable} SourceInternal */
28
- /** @typedef {`${number}-${number}`} GlyphRange */
29
- /** @typedef {'png' | 'mvt' | 'jpg' | 'webp'} TileFormat */
30
- /**
31
- * @typedef {object} SourceInfo
32
- * @property {import('./types.js').SMPSource} source
33
- * @property {string} encodedSourceId
34
- * @property {TileFormat} [format]
35
- */
36
- /**
37
- * @typedef {object} TileInfo
38
- * @property {number} z
39
- * @property {number} x
40
- * @property {number} y
41
- * @property {string} sourceId
42
- * @property {TileFormat} [format]
43
- */
44
- /**
45
- * @typedef {object} GlyphInfo
46
- * @property {string} font
47
- * @property {GlyphRange} range
48
- */
49
-
50
- /** @import { StyleSpecification } from '@maplibre/maplibre-gl-style-spec' */
51
- /** @import { InputSource, SMPSource } from './types.js' */
52
-
53
- export const SUPPORTED_SOURCE_TYPES = /** @type {const} */ ([
54
- 'raster',
55
- 'vector',
56
- 'geojson',
57
- ])
58
-
59
- /**
60
- * Write a styled map package to a stream. Stream `writer.outputStream` to a
61
- * destination, e.g. `fs.createWriteStream('my-map.styledmap')`. You must call
62
- * `witer.finish()` and then wait for your writable stream to `finish` before
63
- * using the output.
64
- */
65
- export default class Writer extends EventEmitter {
66
- #archive = archiver('zip', { zlib: { level: 9 } })
67
- .setMaxListeners(Infinity)
68
- .on('error', console.error)
69
- /** @type {Set<string>} */
70
- #addedFiles = new Set()
71
- /** @type {Set<string>} */
72
- #fonts = new Set()
73
- /** @type {Set<string>} */
74
- #addedSpriteIds = new Set()
75
- /** @type {Map<string, SourceInfo>} */
76
- #sources = new Map()
77
- /** @type {StyleSpecification} */
78
- #style
79
- #outputStream
80
-
81
- static SUPPORTED_SOURCE_TYPES = SUPPORTED_SOURCE_TYPES
82
-
83
- /**
84
- * @param {any} style A v7 or v8 MapLibre style. v7 styles will be migrated to
85
- * v8. (There are currently no typescript declarations for v7 styles, hence
86
- * this is typed as `any` and validated internally)
87
- * @param {object} opts
88
- * @param {number} [opts.highWaterMark=1048576] The maximum number of bytes to buffer during write
89
- */
90
- constructor(style, { highWaterMark = 1024 * 1024 } = {}) {
91
- super()
92
- if (!style || !('version' in style)) {
93
- throw new Error('Invalid style')
94
- }
95
- if (style.version !== 7 && style.version !== 8) {
96
- throw new Error(`Invalid style: Unsupported version v${style.version}`)
97
- }
98
- // Basic validation so migrate can work - more validation is done later
99
- if (!Array.isArray(style.layers)) {
100
- throw new Error('Invalid style: missing layers property')
101
- }
102
-
103
- const styleCopy = clone(style)
104
- // This mutates the style, so we work on a clone
105
- migrate(styleCopy)
106
- const errors = validateStyleMin(styleCopy)
107
- if (errors.length) {
108
- throw new AggregateError(errors, 'Invalid style')
109
- }
110
- this.#style = styleCopy
111
-
112
- for (const [sourceId, source] of Object.entries(this.#style.sources)) {
113
- if (source.type !== 'geojson') continue
114
- // Eagerly add GeoJSON sources - if they reference data via a URL and data
115
- // is not added, these sources will be excluded from the resulting SMP
116
- this.#addSource(sourceId, source)
117
- }
118
-
119
- this.#outputStream = new PassThrough({ highWaterMark })
120
- pipeline(this.#archive, this.#outputStream, (err) => {
121
- if (err) this.emit('error', err)
122
- })
123
- }
124
-
125
- /**
126
- * @returns {import('stream').Readable} Readable stream of the styled map package
127
- */
128
- get outputStream() {
129
- return this.#outputStream
130
- }
131
-
132
- #getBounds() {
133
- /** @type {import('./utils/geo.js').BBox | undefined} */
134
- let bounds
135
- let maxzoom = 0
136
- for (const { source } of this.#sources.values()) {
137
- if (source.type === 'geojson') {
138
- if (isEmptyFeatureCollection(source.data)) continue
139
- // GeoJSON source always increases the bounds of the map
140
- const bbox = get2DBBox(source.data.bbox)
141
- bounds = bounds ? unionBBox([bounds, bbox]) : [...bbox]
142
- } else {
143
- // For raster and vector tile sources, a source with a higher max zoom
144
- // overrides the bounds from lower zooms, because bounds from lower zoom
145
- // tiles do not really reflect actual bounds (imagine a source of zoom 0
146
- // - a single tile covers the whole world)
147
- if (source.maxzoom < maxzoom) continue
148
- if (source.maxzoom === maxzoom) {
149
- bounds = bounds ? unionBBox([bounds, source.bounds]) : source.bounds
150
- } else {
151
- bounds = source.bounds
152
- maxzoom = source.maxzoom
153
- }
154
- }
155
- }
156
- return bounds
157
- }
158
-
159
- #getMaxZoom() {
160
- let maxzoom = 0
161
- for (const { source } of this.#sources.values()) {
162
- const sourceMaxzoom =
163
- // For GeoJSON sources, the maxzoom is 16 unless otherwise set
164
- source.type === 'geojson' ? source.maxzoom || 16 : source.maxzoom
165
- maxzoom = Math.max(maxzoom, sourceMaxzoom)
166
- }
167
- return maxzoom
168
- }
169
-
170
- /**
171
- * Add a source definition to the styled map package
172
- *
173
- * @param {string} sourceId
174
- * @param {InputSource} source
175
- * @returns {SourceInfo}
176
- */
177
- #addSource(sourceId, source) {
178
- const encodedSourceId = encodeSourceId(this.#sources.size)
179
- // Most of the body of this function is just to keep Typescript happy.
180
- // Makes it more verbose, but makes it more type safe.
181
- const tileSourceOverrides = {
182
- minzoom: 0,
183
- maxzoom: 0,
184
- bounds: /** @type {import('./utils/geo.js').BBox} */ ([...MAX_BOUNDS]),
185
- tiles: /** @type {string[]} */ ([]),
186
- }
187
- /** @type {SMPSource} */
188
- let smpSource
189
- switch (source.type) {
190
- case 'raster':
191
- case 'vector':
192
- smpSource = {
193
- ...excludeKeys(source, ['tiles', 'url']),
194
- ...tileSourceOverrides,
195
- }
196
- break
197
- case 'geojson':
198
- smpSource = {
199
- ...source,
200
- maxzoom: 0,
201
- data:
202
- typeof source.data !== 'string'
203
- ? // Add a bbox property to the GeoJSON data if it doesn't already have one
204
- { ...source.data, bbox: source.data.bbox || bbox(source.data) }
205
- : // If GeoJSON data is referenced by a URL, start with an empty FeatureCollection
206
- {
207
- type: 'FeatureCollection',
208
- features: [],
209
- bbox: [0, 0, 0, 0],
210
- },
211
- }
212
- break
213
- }
214
- const sourceInfo = {
215
- source: smpSource,
216
- encodedSourceId,
217
- }
218
- this.#sources.set(sourceId, sourceInfo)
219
- return sourceInfo
220
- }
221
-
222
- /**
223
- * Add a tile to the styled map package
224
- *
225
- * @param {Source} tileData
226
- * @param {TileInfo} opts
227
- */
228
- async addTile(tileData, { z, x, y, sourceId, format }) {
229
- let sourceInfo = this.#sources.get(sourceId)
230
- if (!sourceInfo) {
231
- const source = this.#style.sources[sourceId]
232
- if (!source) {
233
- throw new Error(`Source not referenced in style.json: ${sourceId}`)
234
- }
235
- if (source.type !== 'raster' && source.type !== 'vector') {
236
- throw new Error(`Unsupported source type: ${source.type}`)
237
- }
238
- sourceInfo = this.#addSource(sourceId, source)
239
- }
240
- const { source, encodedSourceId } = sourceInfo
241
- // Mainly to keep Typescript happy...
242
- if (source.type !== 'raster' && source.type !== 'vector') {
243
- throw new Error(`Unsupported source type: ${source.type}`)
244
- }
245
-
246
- if (!format) {
247
- const tileDataStream =
248
- typeof tileData === 'string'
249
- ? fs.createReadStream(tileData)
250
- : tileData instanceof Uint8Array
251
- ? Readable.from(tileData)
252
- : tileData
253
- ;[format, tileData] = await getTileFormatFromStream(tileDataStream)
254
- }
255
-
256
- if (!sourceInfo.format) {
257
- sourceInfo.format = format
258
- } else if (sourceInfo.format !== format) {
259
- throw new Error(
260
- `Tile format mismatch for source ${sourceId}: expected ${sourceInfo.format}, got ${format}`,
261
- )
262
- }
263
-
264
- const bbox = tileToBBox({ z, x, y })
265
- // We calculate the bounds from the tiles at the max zoom level, because at
266
- // lower zooms the tile bbox is much larger than the actual bounding box
267
- if (z > source.maxzoom) {
268
- source.maxzoom = z
269
- source.bounds = bbox
270
- } else if (z === source.maxzoom) {
271
- source.bounds = unionBBox([source.bounds, bbox])
272
- }
273
-
274
- const name = getTileFilename({ sourceId: encodedSourceId, z, x, y, format })
275
- // Tiles are stored without compression, because tiles are normally stored
276
- // as a compressed format.
277
- return this.#append(tileData, { name, store: true })
278
- }
279
-
280
- /**
281
- * Create a write stream for adding tiles to the styled map package
282
- *
283
- * @param {object} opts
284
- * @param {number} [opts.concurrency=16] The number of concurrent writes
285
- *
286
- * @returns
287
- */
288
- createTileWriteStream({ concurrency = 16 } = {}) {
289
- return writeStreamFromAsync(this.addTile.bind(this), { concurrency })
290
- }
291
-
292
- /**
293
- * Add a sprite to the styled map package
294
- *
295
- * @param {object} options
296
- * @param {Source} options.json
297
- * @param {Source} options.png
298
- * @param {number} [options.pixelRatio]
299
- * @param {string} [options.id='default']
300
- * @returns {Promise<void>}
301
- */
302
- async addSprite({ json, png, pixelRatio = 1, id = 'default' }) {
303
- this.#addedSpriteIds.add(id)
304
- const jsonName = getSpriteFilename({ id, pixelRatio, ext: '.json' })
305
- const pngName = getSpriteFilename({ id, pixelRatio, ext: '.png' })
306
- await Promise.all([
307
- this.#append(json, { name: jsonName }),
308
- this.#append(png, { name: pngName }),
309
- ])
310
- }
311
-
312
- /**
313
- * Add glyphs to the styled map package
314
- *
315
- * @param {Source} glyphData
316
- * @param {GlyphInfo} glyphInfo
317
- * @returns {Promise<void>}
318
- */
319
- addGlyphs(glyphData, { font: fontName, range }) {
320
- this.#fonts.add(fontName)
321
- const name = getGlyphFilename({ fontstack: fontName, range })
322
- return this.#append(glyphData, { name })
323
- }
324
-
325
- /**
326
- * Create a write stream for adding glyphs to the styled map package
327
- *
328
- * @param {object} opts
329
- * @param {number} [opts.concurrency=16] The number of concurrent writes
330
- * @returns
331
- */
332
- createGlyphWriteStream({ concurrency = 16 } = {}) {
333
- return writeStreamFromAsync(this.addGlyphs.bind(this), { concurrency })
334
- }
335
-
336
- /**
337
- * Finalize the styled map package and write the style to the archive.
338
- * This method must be called to complete the archive.
339
- * You must wait for your destination write stream to 'finish' before using the output.
340
- */
341
- finish() {
342
- this.#prepareStyle()
343
- const style = JSON.stringify(this.#style)
344
- this.#append(style, { name: STYLE_FILE })
345
- this.#archive.finalize()
346
- }
347
-
348
- /**
349
- * Mutates the style object to prepare it for writing to the archive.
350
- * Deterministic: can be run more than once with the same result.
351
- */
352
- #prepareStyle() {
353
- if (this.#sources.size === 0) {
354
- throw new Error('Missing sources: add at least one source')
355
- }
356
- if (this.#style.glyphs && this.#fonts.size === 0) {
357
- throw new Error(
358
- 'Missing fonts: style references glyphs but no fonts added',
359
- )
360
- }
361
-
362
- // Replace any referenced font stacks with a single font choice based on the
363
- // fonts available in this offline map package.
364
- replaceFontStacks(this.#style, [...this.#fonts])
365
-
366
- // Use a custom URL schema for referencing glyphs and sprites
367
- if (this.#style.glyphs) {
368
- this.#style.glyphs = GLYPH_URI
369
- }
370
- if (typeof this.#style.sprite === 'string') {
371
- if (!this.#addedSpriteIds.has('default')) {
372
- throw new Error(
373
- 'Missing sprite: style references sprite but none added',
374
- )
375
- }
376
- this.#style.sprite = getSpriteUri()
377
- } else if (Array.isArray(this.#style.sprite)) {
378
- this.#style.sprite = this.#style.sprite.map(({ id }) => {
379
- if (!this.#addedSpriteIds.has(id)) {
380
- throw new Error(
381
- `Missing sprite: style references sprite ${id} but none added`,
382
- )
383
- }
384
- return { id, url: getSpriteUri(id) }
385
- })
386
- }
387
-
388
- this.#style.sources = {}
389
- for (const [sourceId, { source, encodedSourceId, format = 'mvt' }] of this
390
- .#sources) {
391
- if (source.type === 'geojson' && isEmptyFeatureCollection(source.data)) {
392
- // Skip empty GeoJSON sources
393
- continue
394
- }
395
- this.#style.sources[sourceId] = source
396
- if (!('tiles' in source)) continue
397
- // Add a tile URL (with custom schema) for each tile source
398
- source.tiles = [getTileUri({ sourceId: encodedSourceId, format })]
399
- }
400
-
401
- this.#style.layers = this.#style.layers.filter(
402
- (layer) => !('source' in layer) || !!this.#style.sources[layer.source],
403
- )
404
-
405
- /** @type {Record<string, any>} */
406
- const metadata = this.#style.metadata || (this.#style.metadata = {})
407
- const bounds = this.#getBounds()
408
- if (bounds) {
409
- metadata['smp:bounds'] = bounds
410
- const [w, s, e, n] = bounds
411
- this.#style.center = [w + (e - w) / 2, s + (n - s) / 2]
412
- }
413
- metadata['smp:maxzoom'] = this.#getMaxZoom()
414
- /** @type {Record<string, string>} */
415
- metadata['smp:sourceFolders'] = {}
416
- for (const [sourceId, { encodedSourceId }] of this.#sources) {
417
- metadata['smp:sourceFolders'][sourceId] = encodedSourceId
418
- }
419
- this.#style.zoom = Math.max(0, this.#getMaxZoom() - 2)
420
- }
421
-
422
- /**
423
- *
424
- * @param {Source} source
425
- * @param {{ name: string, store?: boolean }} options
426
- * @returns {Promise<void>}
427
- */
428
- async #append(source, { name, store = false }) {
429
- if (this.#addedFiles.has(name)) {
430
- throw new Error(`${name} already added`)
431
- }
432
- this.#addedFiles.add(name)
433
- const onAdded = pEvent(
434
- this.#archive,
435
- 'entry',
436
- (entry) => entry.name === name,
437
- )
438
- this.#archive.append(convertSource(source), { name, store })
439
- await onAdded
440
- }
441
- }
442
-
443
- /**
444
- * Simple encoding to keep file names in the Zip as short as possible.
445
- *
446
- * @param {number} sourceIndex
447
- */
448
- function encodeSourceId(sourceIndex) {
449
- return sourceIndex.toString(36)
450
- }
451
-
452
- /**
453
- * Convert a source to a format that can be appended to the archive (which does
454
- * not support Uint8Arrays)
455
- *
456
- * @param {Source} source
457
- * @returns {SourceInternal}
458
- */
459
- function convertSource(source) {
460
- return !Buffer.isBuffer(source) && source instanceof Uint8Array
461
- ? Buffer.from(source.buffer, source.byteOffset, source.length)
462
- : source
463
- }
464
-
465
- /** @param {import('geojson').GeoJSON} data */
466
- function isEmptyFeatureCollection(data) {
467
- return data.type === 'FeatureCollection' && data.features.length === 0
468
- }
469
-
470
- /**
471
- * Strictly a GeoJSON bounding box could be 3D, but we only support 2D bounding
472
- * @param {import('geojson').BBox} bbox
473
- * @returns {import('./utils/geo.js').BBox}
474
- */
475
- function get2DBBox(bbox) {
476
- if (bbox.length === 4) return bbox
477
- return [bbox[0], bbox[1], bbox[3], bbox[4]]
478
- }