styled-map-package 1.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 (60) hide show
  1. package/.github/workflows/node.yml +30 -0
  2. package/.github/workflows/release.yml +47 -0
  3. package/.husky/pre-commit +1 -0
  4. package/.nvmrc +1 -0
  5. package/LICENSE.md +7 -0
  6. package/README.md +28 -0
  7. package/bin/smp-download.js +83 -0
  8. package/bin/smp-view.js +52 -0
  9. package/bin/smp.js +11 -0
  10. package/eslint.config.js +17 -0
  11. package/lib/download.js +114 -0
  12. package/lib/index.js +6 -0
  13. package/lib/reader.js +150 -0
  14. package/lib/reporters.js +92 -0
  15. package/lib/server.js +64 -0
  16. package/lib/style-downloader.js +363 -0
  17. package/lib/tile-downloader.js +188 -0
  18. package/lib/types.ts +104 -0
  19. package/lib/utils/fetch.js +100 -0
  20. package/lib/utils/file-formats.js +85 -0
  21. package/lib/utils/geo.js +87 -0
  22. package/lib/utils/mapbox.js +155 -0
  23. package/lib/utils/misc.js +26 -0
  24. package/lib/utils/streams.js +162 -0
  25. package/lib/utils/style.js +174 -0
  26. package/lib/utils/templates.js +136 -0
  27. package/lib/writer.js +478 -0
  28. package/map-viewer/index.html +89 -0
  29. package/package.json +103 -0
  30. package/test/download-write-read.js +43 -0
  31. package/test/fixtures/invalid-styles/empty.json +1 -0
  32. package/test/fixtures/invalid-styles/missing-source.json +10 -0
  33. package/test/fixtures/invalid-styles/no-layers.json +4 -0
  34. package/test/fixtures/invalid-styles/no-sources.json +4 -0
  35. package/test/fixtures/invalid-styles/null.json +1 -0
  36. package/test/fixtures/invalid-styles/unsupported-version.json +5 -0
  37. package/test/fixtures/valid-styles/external-geojson.input.json +66 -0
  38. package/test/fixtures/valid-styles/external-geojson.output.json +93 -0
  39. package/test/fixtures/valid-styles/inline-geojson.input.json +421 -0
  40. package/test/fixtures/valid-styles/inline-geojson.output.json +1573 -0
  41. package/test/fixtures/valid-styles/maplibre-demotiles.input.json +831 -0
  42. package/test/fixtures/valid-styles/maplibre-unlabelled.input.json +496 -0
  43. package/test/fixtures/valid-styles/maplibre-unlabelled.output.json +1573 -0
  44. package/test/fixtures/valid-styles/minimal-labelled.input.json +37 -0
  45. package/test/fixtures/valid-styles/minimal-labelled.output.json +72 -0
  46. package/test/fixtures/valid-styles/minimal-sprites.input.json +37 -0
  47. package/test/fixtures/valid-styles/minimal-sprites.output.json +58 -0
  48. package/test/fixtures/valid-styles/minimal.input.json +54 -0
  49. package/test/fixtures/valid-styles/minimal.output.json +92 -0
  50. package/test/fixtures/valid-styles/multiple-sprites.input.json +46 -0
  51. package/test/fixtures/valid-styles/multiple-sprites.output.json +128 -0
  52. package/test/fixtures/valid-styles/raster-sources.input.json +33 -0
  53. package/test/fixtures/valid-styles/raster-sources.output.json +69 -0
  54. package/test/utils/assert-bbox-equal.js +19 -0
  55. package/test/utils/digest-stream.js +36 -0
  56. package/test/utils/image-streams.js +30 -0
  57. package/test/utils/reader-helper.js +72 -0
  58. package/test/write-read.js +620 -0
  59. package/tsconfig.json +18 -0
  60. package/types/buffer-peek-stream.d.ts +12 -0
@@ -0,0 +1,620 @@
1
+ import SphericalMercator from '@mapbox/sphericalmercator'
2
+ import { bbox as turfBbox } from '@turf/bbox'
3
+ import randomStream from 'random-bytes-readable-stream'
4
+ import { fromBuffer as zipFromBuffer } from 'yauzl-promise'
5
+
6
+ import assert from 'node:assert/strict'
7
+ import fs from 'node:fs/promises'
8
+ import {
9
+ buffer as streamToBuffer,
10
+ json as streamToJson,
11
+ } from 'node:stream/consumers'
12
+ import { test } from 'node:test'
13
+
14
+ import { Reader, Writer } from '../lib/index.js'
15
+ import { tileIterator } from '../lib/tile-downloader.js'
16
+ import { unionBBox } from '../lib/utils/geo.js'
17
+ import { assertBboxEqual } from './utils/assert-bbox-equal.js'
18
+ import { DigestStream } from './utils/digest-stream.js'
19
+ import { randomImageStream } from './utils/image-streams.js'
20
+ import { ReaderHelper } from './utils/reader-helper.js'
21
+
22
+ /** @import { BBox } from '../lib/utils/geo.js' */
23
+
24
+ /** @param {string | URL} filePath */
25
+ async function readJson(filePath) {
26
+ return JSON.parse(await fs.readFile(filePath, 'utf8'))
27
+ }
28
+
29
+ const updateSnapshots = !!process.env.UPDATE_SNAPSHOTS
30
+
31
+ test('Invalid styles', async (t) => {
32
+ const fixturesDir = new URL('./fixtures/invalid-styles/', import.meta.url)
33
+ const fixtures = await fs.readdir(fixturesDir)
34
+ for (const fixture of fixtures) {
35
+ await t.test(fixture, async () => {
36
+ const stylePath = new URL(fixture, fixturesDir)
37
+ const style = await readJson(stylePath)
38
+ await assert.rejects(
39
+ async () => {
40
+ new Writer(style)
41
+ },
42
+ {
43
+ message: /Invalid style/,
44
+ },
45
+ `Expected ${fixture} to throw an error`,
46
+ )
47
+ })
48
+ }
49
+ })
50
+
51
+ test('Minimal write & read', async () => {
52
+ const styleInUrl = new URL(
53
+ './fixtures/valid-styles/minimal.input.json',
54
+ import.meta.url,
55
+ )
56
+ const styleIn = await readJson(styleInUrl)
57
+ const writer = new Writer(styleIn)
58
+ const sm = new SphericalMercator()
59
+
60
+ const bounds = /** @type {BBox} */ ([-40.6, -50.6, 151.6, 76.0])
61
+ const sourceId = 'maplibre'
62
+ const maxzoom = 5
63
+ const { minX, minY, maxX, maxY } = sm.xyz(bounds, maxzoom)
64
+ const expectedOutputBounds = unionBBox([
65
+ sm.bbox(minX, minY, maxzoom),
66
+ sm.bbox(maxX, maxY, maxzoom),
67
+ ])
68
+
69
+ const tileHashes = new Map()
70
+ for (const { x, y, z } of tileIterator({ maxzoom: 5, bounds })) {
71
+ const stream = randomStream({ size: random(2048, 4096) }).pipe(
72
+ new DigestStream('md5'),
73
+ )
74
+ await writer.addTile(stream, { x, y, z, sourceId, format: 'mvt' })
75
+ const tileId = `${z}/${x}/${y}`
76
+ tileHashes.set(tileId, await stream.digest('hex'))
77
+ }
78
+
79
+ writer.finish()
80
+
81
+ const smp = await streamToBuffer(writer.outputStream)
82
+ const reader = new Reader(await zipFromBuffer(smp))
83
+ const readerHelper = new ReaderHelper(reader)
84
+
85
+ const styleOut = await reader.getStyle()
86
+ compareAndSnapshotStyle({ styleInUrl, styleOut })
87
+
88
+ assertBboxEqual(
89
+ // @ts-expect-error
90
+ styleOut.sources[sourceId].bounds,
91
+ expectedOutputBounds,
92
+ 'Source has correct bounds added',
93
+ )
94
+ assertBboxEqual(
95
+ styleOut.metadata['smp:bounds'],
96
+ expectedOutputBounds,
97
+ 'Style has correct bounds metadata added',
98
+ )
99
+
100
+ for (const { x, y, z } of tileIterator({ maxzoom: 5, bounds })) {
101
+ const hash = await readerHelper.getTileHash({ x, y, z, sourceId })
102
+ assert.equal(
103
+ hash,
104
+ tileHashes.get(`${z}/${x}/${y}`),
105
+ `Tile ${z}/${x}/${y} is the same`,
106
+ )
107
+ }
108
+ })
109
+
110
+ test('Inline GeoJSON is not removed from style', async () => {
111
+ const styleInUrl = new URL(
112
+ './fixtures/valid-styles/inline-geojson.input.json',
113
+ import.meta.url,
114
+ )
115
+ const styleIn = await readJson(styleInUrl)
116
+ const writer = new Writer(styleIn)
117
+
118
+ writer.finish()
119
+
120
+ const smp = await streamToBuffer(writer.outputStream)
121
+ const reader = new Reader(await zipFromBuffer(smp))
122
+
123
+ const styleOut = await reader.getStyle()
124
+ await compareAndSnapshotStyle({ styleInUrl, styleOut })
125
+
126
+ assert.equal(styleOut.sources.crimea.type, 'geojson')
127
+ const { bbox, ...geoJsonOut } = styleOut.sources.crimea.data
128
+ assert.deepEqual(
129
+ geoJsonOut,
130
+ styleIn.sources.crimea.data,
131
+ 'GeoJSON is the same',
132
+ )
133
+ const expectedBbox = turfBbox(styleIn.sources.crimea.data)
134
+ assertBboxEqual(bbox, expectedBbox, 'GeoJSON has correct bbox added')
135
+ assertBboxEqual(
136
+ styleOut.metadata['smp:bounds'],
137
+ expectedBbox,
138
+ 'Style has correct bounds metadata added',
139
+ )
140
+ assert.equal(
141
+ styleOut.metadata['smp:maxzoom'],
142
+ 16,
143
+ 'Style has correct maxzoom metadata added for GeoJSON',
144
+ )
145
+ })
146
+
147
+ test('Un-added source is stripped from output', async () => {
148
+ const styleInUrl = new URL(
149
+ './fixtures/valid-styles/maplibre-unlabelled.input.json',
150
+ import.meta.url,
151
+ )
152
+
153
+ /** @type {import('@maplibre/maplibre-gl-style-spec').StyleSpecification} */
154
+ const styleIn = await readJson(styleInUrl)
155
+ assert('maplibre' in styleIn.sources, 'input style contains maplibre source')
156
+ const styleInGeoJsonSourceEntry = Object.entries(styleIn.sources).find(
157
+ ([, source]) => source.type === 'geojson',
158
+ )
159
+ assert(styleInGeoJsonSourceEntry, 'input style contains geojson source')
160
+ assert(
161
+ styleIn.layers.filter((l) => 'source' in l && l.source === 'maplibre')
162
+ .length > 0,
163
+ 'input style contains layers with maplibre source',
164
+ )
165
+ const writer = new Writer(styleIn)
166
+
167
+ writer.finish()
168
+
169
+ const smp = await streamToBuffer(writer.outputStream)
170
+ const reader = new Reader(await zipFromBuffer(smp))
171
+
172
+ const styleOut = await reader.getStyle()
173
+ await compareAndSnapshotStyle({ styleInUrl, styleOut })
174
+ assert.deepEqual(
175
+ Object.keys(styleOut.sources),
176
+ [styleInGeoJsonSourceEntry[0]],
177
+ 'output style only contains geojson source',
178
+ )
179
+ assert(styleOut.layers.length > 0, 'output style contains layers')
180
+ assert.equal(
181
+ styleOut.layers.filter((l) => 'source' in l && l.source === 'maplibre')
182
+ .length,
183
+ 0,
184
+ 'output style does not contain layers with maplibre source',
185
+ )
186
+ })
187
+
188
+ test('Glyphs can be written and read', async () => {
189
+ const styleInUrl = new URL(
190
+ './fixtures/valid-styles/minimal-labelled.input.json',
191
+ import.meta.url,
192
+ )
193
+ /** @type {import('@maplibre/maplibre-gl-style-spec').StyleSpecification} */
194
+ const styleIn = await readJson(styleInUrl)
195
+ const writer = new Writer(styleIn)
196
+
197
+ assert(typeof styleIn.glyphs === 'string', 'input style has glyphs URL')
198
+ const font = 'Open Sans Semibold'
199
+
200
+ // Need to add at least one tile for the source
201
+ await writer.addTile(randomStream({ size: 1024 }), {
202
+ x: 0,
203
+ y: 0,
204
+ z: 0,
205
+ sourceId: 'maplibre',
206
+ format: 'mvt',
207
+ })
208
+
209
+ /** @type {Map<string, string>} */
210
+ const glyphHashes = new Map()
211
+ for (const range of glyphRanges()) {
212
+ const stream = randomStream({ size: random(256, 1024) }).pipe(
213
+ new DigestStream('md5'),
214
+ )
215
+ await writer.addGlyphs(stream, { range, font })
216
+ glyphHashes.set(range, await stream.digest('hex'))
217
+ }
218
+ writer.finish()
219
+
220
+ const smp = await streamToBuffer(writer.outputStream)
221
+ const reader = new Reader(await zipFromBuffer(smp))
222
+ const readerHelper = new ReaderHelper(reader)
223
+
224
+ const styleOut = await reader.getStyle()
225
+ await compareAndSnapshotStyle({ styleInUrl, styleOut })
226
+
227
+ for (const range of glyphRanges()) {
228
+ const hash = await readerHelper.getGlyphHash({ range, font })
229
+ assert.equal(
230
+ hash,
231
+ glyphHashes.get(range),
232
+ `Glyphs for ${range} are the same`,
233
+ )
234
+ }
235
+ })
236
+
237
+ test('Missing glyphs throws an error', async () => {
238
+ const styleInUrl = new URL(
239
+ './fixtures/valid-styles/minimal-labelled.input.json',
240
+ import.meta.url,
241
+ )
242
+ /** @type {import('@maplibre/maplibre-gl-style-spec').StyleSpecification} */
243
+ const styleIn = await readJson(styleInUrl)
244
+ const writer = new Writer(styleIn)
245
+
246
+ assert(typeof styleIn.glyphs === 'string', 'input style has glyphs URL')
247
+
248
+ // Need to add at least one tile for the source
249
+ await writer.addTile(randomStream({ size: 1024 }), {
250
+ x: 0,
251
+ y: 0,
252
+ z: 0,
253
+ sourceId: 'maplibre',
254
+ format: 'mvt',
255
+ })
256
+
257
+ await assert.rejects(async () => writer.finish(), {
258
+ message: /Missing fonts/,
259
+ })
260
+ })
261
+
262
+ test('Finishing writer with no sources throws and error', async () => {
263
+ const styleInUrl = new URL(
264
+ './fixtures/valid-styles/minimal.input.json',
265
+ import.meta.url,
266
+ )
267
+ const styleIn = await readJson(styleInUrl)
268
+ const writer = new Writer(styleIn)
269
+
270
+ await assert.rejects(async () => writer.finish(), {
271
+ message: /Missing sources/,
272
+ })
273
+ })
274
+
275
+ test('External GeoJSON & layers that use it are excluded if not added', async () => {
276
+ const styleInUrl = new URL(
277
+ './fixtures/valid-styles/external-geojson.input.json',
278
+ import.meta.url,
279
+ )
280
+ /** @type {import('@maplibre/maplibre-gl-style-spec').StyleSpecification} */
281
+ const styleIn = await readJson(styleInUrl)
282
+ const writer = new Writer(styleIn)
283
+
284
+ assert(
285
+ 'crimea' in styleIn.sources && styleIn.sources.crimea.type === 'geojson',
286
+ 'input style contains crimea geojson source',
287
+ )
288
+ assert.equal(
289
+ typeof styleIn.sources.crimea.data,
290
+ 'string',
291
+ 'geojson source is external (data is URL)',
292
+ )
293
+ assert(
294
+ styleIn.layers.find((l) => 'source' in l && l.source === 'crimea'),
295
+ 'input style contains layers with crimea source',
296
+ )
297
+
298
+ // Need to add at least one tile for the source
299
+ await writer.addTile(randomStream({ size: 1024 }), {
300
+ x: 0,
301
+ y: 0,
302
+ z: 0,
303
+ sourceId: 'maplibre',
304
+ format: 'mvt',
305
+ })
306
+
307
+ writer.finish()
308
+
309
+ const smp = await streamToBuffer(writer.outputStream)
310
+ const reader = new Reader(await zipFromBuffer(smp))
311
+
312
+ const styleOut = await reader.getStyle()
313
+ await compareAndSnapshotStyle({ styleInUrl, styleOut })
314
+
315
+ assert(
316
+ !('crimea' in styleOut.sources),
317
+ 'output style does not contain crimea geojson source',
318
+ )
319
+ assert(
320
+ !styleOut.layers.find((l) => 'source' in l && l.source === 'crimea'),
321
+ 'output style does not contain layers with crimea source',
322
+ )
323
+ })
324
+
325
+ test('Missing sprites throws an error', async () => {
326
+ const styleInUrl = new URL(
327
+ './fixtures/valid-styles/minimal-sprites.input.json',
328
+ import.meta.url,
329
+ )
330
+ /** @type {import('@maplibre/maplibre-gl-style-spec').StyleSpecification} */
331
+ const styleIn = await readJson(styleInUrl)
332
+ const writer = new Writer(styleIn)
333
+
334
+ assert(typeof styleIn.sprite === 'string', 'input style has sprite URL')
335
+
336
+ // Need to add at least one tile for the source
337
+ await writer.addTile(randomStream({ size: 1024 }), {
338
+ x: 0,
339
+ y: 0,
340
+ z: 0,
341
+ sourceId: 'openmaptiles',
342
+ format: 'mvt',
343
+ })
344
+
345
+ await assert.rejects(async () => writer.finish(), {
346
+ message: /Missing sprite/,
347
+ })
348
+ })
349
+
350
+ test('Can write and read sprites', async () => {
351
+ const styleInUrl = new URL(
352
+ './fixtures/valid-styles/minimal-sprites.input.json',
353
+ import.meta.url,
354
+ )
355
+ /** @type {import('@maplibre/maplibre-gl-style-spec').StyleSpecification} */
356
+ const styleIn = await readJson(styleInUrl)
357
+ const writer = new Writer(styleIn)
358
+
359
+ assert(typeof styleIn.sprite === 'string', 'input style has sprite URL')
360
+
361
+ // Need to add at least one tile for the source
362
+ await writer.addTile(randomStream({ size: 1024 }), {
363
+ x: 0,
364
+ y: 0,
365
+ z: 0,
366
+ sourceId: 'openmaptiles',
367
+ format: 'mvt',
368
+ })
369
+
370
+ const sprite1xImageStream = randomStream({ size: random(1024, 2048) }).pipe(
371
+ new DigestStream('md5'),
372
+ )
373
+ const sprite2xImageStream = randomStream({ size: random(1024, 2048) }).pipe(
374
+ new DigestStream('md5'),
375
+ )
376
+ const spriteLayoutIn = {
377
+ airfield_11: {
378
+ height: 17,
379
+ pixelRatio: 1,
380
+ width: 17,
381
+ x: 21,
382
+ y: 0,
383
+ },
384
+ }
385
+ await writer.addSprite({
386
+ png: sprite1xImageStream,
387
+ json: JSON.stringify(spriteLayoutIn),
388
+ })
389
+ const sprite1xImageHash = await sprite1xImageStream.digest('hex')
390
+ await writer.addSprite({
391
+ png: sprite2xImageStream,
392
+ json: JSON.stringify(spriteLayoutIn),
393
+ pixelRatio: 2,
394
+ })
395
+ const sprite2xImageHash = await sprite2xImageStream.digest('hex')
396
+
397
+ writer.finish()
398
+
399
+ const smp = await streamToBuffer(writer.outputStream)
400
+ const reader = new Reader(await zipFromBuffer(smp))
401
+ const readerHelper = new ReaderHelper(reader)
402
+
403
+ const styleOut = await reader.getStyle('')
404
+ await compareAndSnapshotStyle({ styleInUrl, styleOut })
405
+
406
+ const sprite1xImageHashOut = await readerHelper.getSpriteHash({ ext: 'png' })
407
+ const sprite2xImageHashOut = await readerHelper.getSpriteHash({
408
+ ext: 'png',
409
+ pixelRatio: 2,
410
+ })
411
+ const spriteJsonResource = await reader.getResource(styleOut.sprite + '.json')
412
+ assert.equal(
413
+ spriteJsonResource.contentType,
414
+ 'application/json; charset=utf-8',
415
+ )
416
+ const spriteLayoutOut = await streamToJson(spriteJsonResource.stream)
417
+
418
+ assert.equal(
419
+ sprite1xImageHashOut,
420
+ sprite1xImageHash,
421
+ 'Sprite image is the same',
422
+ )
423
+ assert.equal(
424
+ sprite2xImageHashOut,
425
+ sprite2xImageHash,
426
+ 'Sprite @2x image is the same',
427
+ )
428
+ assert.deepEqual(spriteLayoutOut, spriteLayoutIn, 'Sprite layout is the same')
429
+ })
430
+
431
+ test('Can write and read style with multiple sprites', async () => {
432
+ const styleInUrl = new URL(
433
+ './fixtures/valid-styles/multiple-sprites.input.json',
434
+ import.meta.url,
435
+ )
436
+ /** @type {import('@maplibre/maplibre-gl-style-spec').StyleSpecification} */
437
+ const styleIn = await readJson(styleInUrl)
438
+ const writer = new Writer(styleIn)
439
+
440
+ assert(Array.isArray(styleIn.sprite), 'input style has array of sprites')
441
+
442
+ // Need to add at least one tile for the source
443
+ await writer.addTile(randomStream({ size: 1024 }), {
444
+ x: 0,
445
+ y: 0,
446
+ z: 0,
447
+ sourceId: 'openmaptiles',
448
+ format: 'mvt',
449
+ })
450
+
451
+ const spriteRoadsignsImageStream = randomStream({
452
+ size: random(1024, 2048),
453
+ }).pipe(new DigestStream('md5'))
454
+ const spriteDefaultImageStream = randomStream({
455
+ size: random(1024, 2048),
456
+ }).pipe(new DigestStream('md5'))
457
+ const spriteRoadsignsLayoutIn = {
458
+ airfield_11: {
459
+ height: 17,
460
+ pixelRatio: 1,
461
+ width: 17,
462
+ x: 21,
463
+ y: 0,
464
+ },
465
+ }
466
+ const spriteDefaultLayoutIn = {
467
+ other_sprite: {
468
+ height: 17,
469
+ pixelRatio: 1,
470
+ width: 17,
471
+ x: 21,
472
+ y: 0,
473
+ },
474
+ }
475
+ await writer.addSprite({
476
+ png: spriteDefaultImageStream,
477
+ json: JSON.stringify(spriteDefaultLayoutIn),
478
+ })
479
+ const spriteDefaultImageHash = await spriteDefaultImageStream.digest('hex')
480
+ await writer.addSprite({
481
+ id: 'roadsigns',
482
+ png: spriteRoadsignsImageStream,
483
+ json: JSON.stringify(spriteRoadsignsLayoutIn),
484
+ })
485
+ const spriteRoadsignsImageHash =
486
+ await spriteRoadsignsImageStream.digest('hex')
487
+
488
+ writer.finish()
489
+
490
+ const smp = await streamToBuffer(writer.outputStream)
491
+ const reader = new Reader(await zipFromBuffer(smp))
492
+ const readerHelper = new ReaderHelper(reader)
493
+
494
+ const styleOut = await reader.getStyle('')
495
+ await compareAndSnapshotStyle({ styleInUrl, styleOut })
496
+
497
+ const spriteDefaultImageHashOut = await readerHelper.getSpriteHash({
498
+ ext: 'png',
499
+ })
500
+ const spriteRoadsignsImageHashOut = await readerHelper.getSpriteHash({
501
+ ext: 'png',
502
+ id: 'roadsigns',
503
+ })
504
+ // @ts-expect-error
505
+ const defaultUrl = styleOut.sprite.find((s) => s.id === 'default').url
506
+ // @ts-expect-error
507
+ const roadsignsUrl = styleOut.sprite.find((s) => s.id === 'roadsigns').url
508
+ const defaultJsonResource = await reader.getResource(defaultUrl + '.json')
509
+ const roadsignsJsonResource = await reader.getResource(roadsignsUrl + '.json')
510
+ const defaultLayoutOut = await streamToJson(defaultJsonResource.stream)
511
+ const roadsignsLayoutOut = await streamToJson(roadsignsJsonResource.stream)
512
+
513
+ assert.equal(
514
+ spriteDefaultImageHashOut,
515
+ spriteDefaultImageHash,
516
+ 'Sprite image is the same',
517
+ )
518
+ assert.equal(
519
+ spriteRoadsignsImageHashOut,
520
+ spriteRoadsignsImageHash,
521
+ 'Sprite @2x image is the same',
522
+ )
523
+ assert.deepEqual(
524
+ defaultLayoutOut,
525
+ spriteDefaultLayoutIn,
526
+ 'Sprite layout is the same',
527
+ )
528
+ assert.deepEqual(
529
+ roadsignsLayoutOut,
530
+ spriteRoadsignsLayoutIn,
531
+ 'Sprite layout is the same',
532
+ )
533
+ })
534
+
535
+ test('Raster tiles write and read', async () => {
536
+ const styleInUrl = new URL(
537
+ './fixtures/valid-styles/raster-sources.input.json',
538
+ import.meta.url,
539
+ )
540
+ const styleIn = await readJson(styleInUrl)
541
+ const writer = new Writer(styleIn)
542
+
543
+ const pngStream = randomImageStream({
544
+ width: 256,
545
+ height: 256,
546
+ format: 'png',
547
+ }).pipe(new DigestStream('md5'))
548
+ const jpgStream = randomImageStream({
549
+ width: 256,
550
+ height: 256,
551
+ format: 'jpg',
552
+ }).pipe(new DigestStream('md5'))
553
+ const pngTileId = { x: 0, y: 0, z: 0, sourceId: 'png-tiles' }
554
+ const jpgTileId = { x: 0, y: 0, z: 0, sourceId: 'jpg-tiles' }
555
+ await writer.addTile(pngStream, { ...pngTileId, format: 'png' })
556
+ await writer.addTile(jpgStream, { ...jpgTileId, format: 'jpg' })
557
+ const pngTileHash = await pngStream.digest('hex')
558
+ const jpgTileHash = await jpgStream.digest('hex')
559
+
560
+ writer.finish()
561
+
562
+ const smp = await streamToBuffer(writer.outputStream)
563
+ const reader = new Reader(await zipFromBuffer(smp))
564
+ const readerHelper = new ReaderHelper(reader)
565
+
566
+ const styleOut = await reader.getStyle()
567
+ compareAndSnapshotStyle({ styleInUrl, styleOut })
568
+
569
+ const pngTileHashOut = await readerHelper.getTileHash(pngTileId)
570
+ const jpgTileHashOut = await readerHelper.getTileHash(jpgTileId)
571
+
572
+ assert.equal(pngTileHashOut, pngTileHash, 'PNG tile is the same')
573
+ assert.equal(jpgTileHashOut, jpgTileHash, 'JPG tile is the same')
574
+ })
575
+
576
+ /**
577
+ *
578
+ * @param {number} min
579
+ * @param {number} max
580
+ * @returns
581
+ */
582
+ function random(min, max) {
583
+ return Math.floor(Math.random() * (max - min + 1)) + min
584
+ }
585
+
586
+ /**
587
+ * @param {{ styleInUrl: URL, styleOut: import('../lib/types.js').SMPStyle }} opts
588
+ */
589
+ async function compareAndSnapshotStyle({ styleInUrl, styleOut }) {
590
+ const snapshotUrl = new URL(
591
+ styleInUrl.pathname.replace(/(\.input)?\.json$/, '.output.json'),
592
+ import.meta.url,
593
+ )
594
+ if (styleInUrl.pathname === snapshotUrl.pathname) {
595
+ throw new Error('Snapshot URL is the same as input')
596
+ }
597
+ if (updateSnapshots) {
598
+ await fs.writeFile(snapshotUrl, JSON.stringify(styleOut, null, 2))
599
+ } else {
600
+ try {
601
+ const expected = await readJson(snapshotUrl)
602
+ assert.deepEqual(styleOut, expected)
603
+ } catch (e) {
604
+ if (e instanceof Error && 'code' in e && e.code === 'ENOENT') {
605
+ await fs.writeFile(snapshotUrl, JSON.stringify(styleOut, null, 2))
606
+ }
607
+ }
608
+ }
609
+ }
610
+
611
+ /**
612
+ *
613
+ * @param {number} max
614
+ * @returns {Generator<`${number}-${number}`>}
615
+ */
616
+ function* glyphRanges(max = Math.pow(2, 16)) {
617
+ for (let i = 0; i < max; i += 256) {
618
+ yield `${i}-${i + 255}`
619
+ }
620
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2021",
4
+ "lib": ["ES2022", "dom"],
5
+ "strict": true,
6
+ "allowSyntheticDefaultImports": true,
7
+ "resolveJsonModule": true,
8
+ "module": "ES2022",
9
+ "moduleResolution": "node",
10
+ "allowJs": true,
11
+ "checkJs": true,
12
+ "noEmit": true,
13
+ "skipLibCheck": true,
14
+ "typeRoots": ["types", "node_modules/@types"]
15
+ },
16
+ "include": ["**/*"],
17
+ "exclude": ["node_modules"]
18
+ }
@@ -0,0 +1,12 @@
1
+ declare module 'buffer-peek-stream' {
2
+ import { Readable } from 'stream'
3
+ interface BufferPeekStream {
4
+ (): void
5
+ promise(
6
+ readStream: Readable,
7
+ bytes: number,
8
+ ): Promise<[Uint8Array, Readable]>
9
+ }
10
+ const bufferPeekStream: BufferPeekStream
11
+ export = bufferPeekStream
12
+ }