styled-map-package-api 5.0.0-pre.2 → 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 -108
  47. package/dist/from-mbtiles.d.cts +0 -14
  48. package/dist/from-mbtiles.js +0 -84
  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-qfyJk4ot.d.cts +0 -200
  65. package/dist/types-qfyJk4ot.d.ts +0 -200
  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,125 @@
1
+ import { StyleDownloader } from './style-downloader.js'
2
+ import { readableFromAsync } from './utils/streams.js'
3
+ import { Writer } from './writer.js'
4
+
5
+ /**
6
+ * @typedef {object} DownloadProgress
7
+ * @property {import('./tile-downloader.js').TileDownloadStats & { done: boolean }} tiles
8
+ * @property {{ done: boolean }} style
9
+ * @property {{ downloaded: number, done: boolean }} sprites
10
+ * @property {import('./style-downloader.js').GlyphDownloadStats & { done: boolean }} glyphs
11
+ * @property {{ totalBytes: number, done: boolean }} output
12
+ * @property {number} elapsedMs
13
+ */
14
+
15
+ /**
16
+ * Download a map style and its resources for a given bounding box and max zoom
17
+ * level. Returns a readable stream of a "styled map package", a zip file
18
+ * containing all the resources needed to serve the style offline.
19
+ *
20
+ * @param {object} opts
21
+ * @param {Readonly<import("./utils/geo.js").BBox>} opts.bbox Bounding box to download tiles for
22
+ * @param {number} opts.maxzoom Max zoom level to download tiles for
23
+ * @param {string} opts.styleUrl URL of the style to download
24
+ * @param { (progress: DownloadProgress) => void } [opts.onprogress] Optional callback for reporting progress
25
+ * @param {string} [opts.accessToken]
26
+ * @param {boolean} [opts.skipLocalGlyphs] Skip glyph ranges rendered client-side by MapLibre GL via localIdeographFontFamily (CJK, Hangul, Kana, Yi, etc.)
27
+ * @param {boolean} [opts.dedupe] When true, duplicate tiles are stored only once (see {@link Writer})
28
+ * @returns {import('./types.js').DownloadStream} Readable stream of the output styled map file
29
+ */
30
+ export function download({
31
+ bbox,
32
+ maxzoom,
33
+ styleUrl,
34
+ onprogress,
35
+ accessToken,
36
+ skipLocalGlyphs,
37
+ dedupe,
38
+ }) {
39
+ const downloader = new StyleDownloader(styleUrl, {
40
+ concurrency: 24,
41
+ mapboxAccessToken: accessToken,
42
+ })
43
+
44
+ let start = Date.now()
45
+ /** @type {DownloadProgress} */
46
+ let progress = {
47
+ tiles: { downloaded: 0, totalBytes: 0, total: 0, skipped: 0, done: false },
48
+ style: { done: false },
49
+ sprites: { downloaded: 0, done: false },
50
+ glyphs: { downloaded: 0, total: 0, totalBytes: 0, done: false },
51
+ output: { totalBytes: 0, done: false },
52
+ elapsedMs: 0,
53
+ }
54
+
55
+ /** @param {Partial<DownloadProgress>} update */
56
+ function handleProgress(update) {
57
+ progress = { ...progress, ...update, elapsedMs: Date.now() - start }
58
+ onprogress?.(progress)
59
+ }
60
+
61
+ const sizeCounter = new TransformStream({
62
+ transform(chunk, controller) {
63
+ handleProgress({
64
+ output: {
65
+ totalBytes: progress.output.totalBytes + chunk.byteLength,
66
+ done: false,
67
+ },
68
+ })
69
+ controller.enqueue(chunk)
70
+ },
71
+ flush() {
72
+ handleProgress({ output: { ...progress.output, done: true } })
73
+ },
74
+ })
75
+
76
+ ;(async () => {
77
+ /** @type {Writer | undefined} */
78
+ let writer
79
+ try {
80
+ const style = await downloader.getStyle()
81
+ writer = new Writer(style, { dedupe: !!dedupe })
82
+ handleProgress({ style: { done: true } })
83
+ // Pipe the output stream through the size counter (fire-and-forget;
84
+ // errors propagate via writer.abort())
85
+ writer.outputStream.pipeTo(sizeCounter.writable).catch(() => {})
86
+
87
+ for await (const spriteInfo of downloader.getSprites()) {
88
+ await writer.addSprite(spriteInfo)
89
+ handleProgress({
90
+ sprites: { downloaded: progress.sprites.downloaded + 1, done: false },
91
+ })
92
+ }
93
+ handleProgress({ sprites: { ...progress.sprites, done: true } })
94
+
95
+ const tiles = downloader.getTiles({
96
+ bounds: bbox,
97
+ maxzoom,
98
+ onprogress: (tileStats) =>
99
+ handleProgress({ tiles: { ...tileStats, done: false } }),
100
+ })
101
+ await readableFromAsync(tiles).pipeTo(
102
+ writer.createTileWriteStream({ concurrency: 24 }),
103
+ )
104
+ handleProgress({ tiles: { ...progress.tiles, done: true } })
105
+
106
+ const glyphs = downloader.getGlyphs({
107
+ skipLocalGlyphs,
108
+ onprogress: (glyphStats) =>
109
+ handleProgress({ glyphs: { ...glyphStats, done: false } }),
110
+ })
111
+ await readableFromAsync(glyphs).pipeTo(writer.createGlyphWriteStream())
112
+ handleProgress({ glyphs: { ...progress.glyphs, done: true } })
113
+
114
+ writer.finish()
115
+ } catch (err) {
116
+ if (writer) {
117
+ writer.abort(/** @type {Error} */ (err))
118
+ } else {
119
+ sizeCounter.writable.abort(/** @type {Error} */ (err))
120
+ }
121
+ }
122
+ })()
123
+
124
+ return sizeCounter.readable
125
+ }
@@ -0,0 +1,157 @@
1
+ /** @import { SMPSource } from './types.js' */
2
+
3
+ /**
4
+ * Minimal valid 1×1 transparent PNG (67 bytes).
5
+ * @type {Uint8Array}
6
+ */
7
+ const EMPTY_PNG = /* @__PURE__ */ fromHex(
8
+ '89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489' +
9
+ '0000000a49444154789c626000000002000198e195280000000049454e44ae426082',
10
+ )
11
+
12
+ /**
13
+ * Minimal valid 1×1 transparent WebP (VP8L lossless, 34 bytes).
14
+ * @type {Uint8Array}
15
+ */
16
+ const EMPTY_WEBP = /* @__PURE__ */ fromHex(
17
+ '524946461a000000574542505650384c0d0000002f00000010071011118888fe0700',
18
+ )
19
+
20
+ /**
21
+ * Empty gzip stream (gzipped empty buffer, 20 bytes). Used for empty MVT
22
+ * tiles and empty glyph PBF ranges.
23
+ * @type {Uint8Array}
24
+ */
25
+ const EMPTY_GZ = /* @__PURE__ */ fromHex(
26
+ '1f8b080000000000001303000000000000000000',
27
+ )
28
+
29
+ /**
30
+ * Empty GeoJSON FeatureCollection as UTF-8 bytes.
31
+ * @type {Uint8Array}
32
+ */
33
+ const EMPTY_JSON = /* @__PURE__ */ new TextEncoder().encode(
34
+ '{"type":"FeatureCollection","features":[]}',
35
+ )
36
+
37
+ /** @type {Record<string, { body: Uint8Array, contentType: string, contentEncoding?: string }>} */
38
+ const TILE_FORMATS = {
39
+ mvt: {
40
+ body: EMPTY_GZ,
41
+ contentType: 'application/vnd.mapbox-vector-tile',
42
+ contentEncoding: 'gzip',
43
+ },
44
+ png: {
45
+ body: EMPTY_PNG,
46
+ contentType: 'image/png',
47
+ },
48
+ jpg: {
49
+ // No such thing as "empty" JPEG — serve a transparent PNG instead.
50
+ // MapLibre handles the content-type mismatch gracefully.
51
+ body: EMPTY_PNG,
52
+ contentType: 'image/png',
53
+ },
54
+ webp: {
55
+ body: EMPTY_WEBP,
56
+ contentType: 'image/webp',
57
+ },
58
+ json: {
59
+ body: EMPTY_JSON,
60
+ contentType: 'application/json; charset=utf-8',
61
+ },
62
+ }
63
+
64
+ /**
65
+ * Detect the tile format from a source's tile URL template.
66
+ *
67
+ * @param {SMPSource} source
68
+ * @returns {string | null}
69
+ */
70
+ function detectTileFormat(source) {
71
+ const tiles = 'tiles' in source ? source.tiles : undefined
72
+ if (!tiles || tiles.length === 0) return null
73
+ const url = tiles[0]
74
+ if (
75
+ url.endsWith('.mvt.gz') ||
76
+ url.endsWith('.mvt') ||
77
+ url.endsWith('.pbf.gz') ||
78
+ url.endsWith('.pbf')
79
+ )
80
+ return 'mvt'
81
+ if (url.endsWith('.png')) return 'png'
82
+ if (url.endsWith('.jpg') || url.endsWith('.jpeg')) return 'jpg'
83
+ if (url.endsWith('.webp')) return 'webp'
84
+ if (url.endsWith('.json') || url.endsWith('.geojson')) return 'json'
85
+ // Default based on source type
86
+ if (source.type === 'vector') return 'mvt'
87
+ if (source.type === 'raster') return 'png'
88
+ return null
89
+ }
90
+
91
+ /**
92
+ * Fallback tile handler for use with `createServer({ fallbackTile })`.
93
+ * Returns an appropriate empty tile based on the source's tile format:
94
+ * - vector sources → empty gzipped MVT
95
+ * - raster PNG sources → 1×1 transparent PNG
96
+ * - raster WebP sources → 1×1 transparent WebP
97
+ * - raster JPEG sources → 1×1 transparent PNG (no such thing as transparent JPEG)
98
+ *
99
+ * @param {{ x: number, y: number, z: number }} _tileId
100
+ * @param {{ sourceId: string, source: SMPSource }} sourceInfo
101
+ * @returns {Response}
102
+ */
103
+ export function emptyTileFallback(_tileId, { source }) {
104
+ const format = detectTileFormat(source)
105
+ const tile = format && TILE_FORMATS[format]
106
+ if (!tile) {
107
+ return new Response('Not Found', { status: 404 })
108
+ }
109
+ /** @type {HeadersInit} */
110
+ const headers = {
111
+ 'Content-Type': tile.contentType,
112
+ 'Content-Length': String(tile.body.byteLength),
113
+ 'Cache-Control': 'public, max-age=604800',
114
+ }
115
+ if (tile.contentEncoding) {
116
+ headers['Content-Encoding'] = tile.contentEncoding
117
+ }
118
+ return new Response(/** @type {BodyInit} */ (tile.body), {
119
+ status: 200,
120
+ headers,
121
+ })
122
+ }
123
+
124
+ /**
125
+ * Fallback glyph handler for use with `createServer({ fallbackGlyph })`.
126
+ * Returns an empty gzipped PBF (valid protobuf with no glyph entries), which
127
+ * causes MapLibre to render missing characters as blank space instead of
128
+ * erroring on a 404.
129
+ *
130
+ * @param {string} _fontstack
131
+ * @param {string} _range
132
+ * @returns {Response}
133
+ */
134
+ // eslint-disable-next-line no-unused-vars
135
+ export function emptyGlyphFallback(_fontstack, _range) {
136
+ return new Response(/** @type {BodyInit} */ (EMPTY_GZ), {
137
+ status: 200,
138
+ headers: {
139
+ 'Content-Type': 'application/x-protobuf',
140
+ 'Content-Encoding': 'gzip',
141
+ 'Content-Length': String(EMPTY_GZ.byteLength),
142
+ 'Cache-Control': 'public, max-age=604800',
143
+ },
144
+ })
145
+ }
146
+
147
+ /**
148
+ * @param {string} hex
149
+ * @returns {Uint8Array}
150
+ */
151
+ function fromHex(hex) {
152
+ const bytes = new Uint8Array(hex.length / 2)
153
+ for (let i = 0; i < hex.length; i += 2) {
154
+ bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16)
155
+ }
156
+ return bytes
157
+ }
@@ -0,0 +1,131 @@
1
+ import { MBTiles } from 'mbtiles-reader'
2
+
3
+ import { noop } from './utils/misc.js'
4
+ import { readableFromAsync } from './utils/streams.js'
5
+ import { Writer } from './writer.js'
6
+
7
+ const SOURCE_ID = 'mbtiles-source'
8
+
9
+ /**
10
+ * Convert a MBTiles file to a styled map package, returned as a web
11
+ * ReadableStream. The async MBTiles.open() happens lazily inside the
12
+ * stream's start(), so this function is synchronous.
13
+ *
14
+ * Requires Node >= 20 (uses better-sqlite3 which dropped Node 18 support).
15
+ *
16
+ * @param {string | ArrayBuffer | Uint8Array} source MBTiles source — file path
17
+ * (Node), OPFS path (browser Worker), or in-memory buffer.
18
+ * @returns {ReadableStream<Uint8Array>}
19
+ */
20
+ export function fromMBTiles(source) {
21
+ /** @type {ReadableStreamDefaultReader<Uint8Array> | undefined} */
22
+ let outputReader
23
+ /** @type {Promise<void> | undefined} */
24
+ let conversionDone
25
+ const pipeAbort = new AbortController()
26
+
27
+ return new ReadableStream({
28
+ async start() {
29
+ const reader = await MBTiles.open(source)
30
+ if (reader.metadata.format === 'pbf') {
31
+ throw new Error('Vector MBTiles are not yet supported')
32
+ }
33
+ const {
34
+ name,
35
+ minzoom,
36
+ maxzoom,
37
+ scheme,
38
+ attribution,
39
+ description,
40
+ version: tilesetVersion,
41
+ } = reader.metadata
42
+ /** @type {import('@maplibre/maplibre-gl-style-spec').StyleSpecification} */
43
+ const style = {
44
+ version: 8,
45
+ name,
46
+ sources: {
47
+ [SOURCE_ID]: {
48
+ type: 'raster',
49
+ tileSize: 256,
50
+ minzoom,
51
+ maxzoom,
52
+ scheme,
53
+ attribution,
54
+ },
55
+ },
56
+ layers: [
57
+ {
58
+ id: 'background',
59
+ type: 'background',
60
+ paint: {
61
+ 'background-color': 'white',
62
+ },
63
+ },
64
+ {
65
+ id: 'raster',
66
+ type: 'raster',
67
+ source: SOURCE_ID,
68
+ paint: {
69
+ 'raster-opacity': 1,
70
+ },
71
+ },
72
+ ],
73
+ }
74
+
75
+ if (description || tilesetVersion) {
76
+ style.metadata = {
77
+ 'mbtiles:description': description,
78
+ 'mbtiles:version': tilesetVersion,
79
+ }
80
+ }
81
+
82
+ const writer = new Writer(style)
83
+ outputReader = writer.outputStream.getReader()
84
+
85
+ conversionDone = (async () => {
86
+ try {
87
+ await readableFromAsync(mbtilesToTileArgs(reader)).pipeTo(
88
+ writer.createTileWriteStream(),
89
+ { signal: pipeAbort.signal },
90
+ )
91
+ writer.finish()
92
+ } catch (err) {
93
+ try {
94
+ writer.abort(err instanceof Error ? err : new Error(String(err)))
95
+ } catch {
96
+ // Output stream may already be cancelled/errored
97
+ }
98
+ }
99
+ })()
100
+ },
101
+ async pull(controller) {
102
+ const { done, value } =
103
+ await /** @type {ReadableStreamDefaultReader<Uint8Array>} */ (
104
+ outputReader
105
+ ).read()
106
+ if (done) {
107
+ controller.close()
108
+ } else {
109
+ controller.enqueue(value)
110
+ }
111
+ },
112
+ async cancel(reason) {
113
+ pipeAbort.abort(reason)
114
+ await conversionDone
115
+ await /** @type {ReadableStreamDefaultReader<Uint8Array>} */ (
116
+ outputReader
117
+ )
118
+ .cancel(reason)
119
+ .catch(noop)
120
+ },
121
+ })
122
+ }
123
+
124
+ /**
125
+ * @param {MBTiles} mbtiles
126
+ */
127
+ async function* mbtilesToTileArgs(mbtiles) {
128
+ for (const { z, x, y, data, format } of mbtiles) {
129
+ yield [data, { z, x, y, format, sourceId: SOURCE_ID }]
130
+ }
131
+ }
package/lib/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /** @typedef {import('./types.js').SMPSource} SMPSource */
2
+ /** @typedef {import('./types.js').SMPStyle} SMPStyle */
3
+
4
+ export { Reader } from './reader.js'
5
+ export { Writer } from './writer.js'
6
+ export { createServer } from './server.js'
7
+ export { StyleDownloader } from './style-downloader.js'
8
+ export { downloadTiles } from './tile-downloader.js'
9
+ export { download } from './download.js'
10
+ export { fromMBTiles } from './from-mbtiles.js'
11
+ export { emptyTileFallback, emptyGlyphFallback } from './fallbacks.js'
12
+ export { validate } from './validator.js'