styled-map-package-api 5.0.0-pre.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 (71) hide show
  1. package/dist/download.cjs +100 -0
  2. package/dist/download.d.cts +64 -0
  3. package/dist/download.d.ts +64 -0
  4. package/dist/download.js +76 -0
  5. package/dist/from-mbtiles.cjs +81 -0
  6. package/dist/from-mbtiles.d.cts +10 -0
  7. package/dist/from-mbtiles.d.ts +10 -0
  8. package/dist/from-mbtiles.js +57 -0
  9. package/dist/index.cjs +46 -0
  10. package/dist/index.d.cts +25 -0
  11. package/dist/index.d.ts +25 -0
  12. package/dist/index.js +16 -0
  13. package/dist/reader.cjs +287 -0
  14. package/dist/reader.d.cts +68 -0
  15. package/dist/reader.d.ts +68 -0
  16. package/dist/reader.js +259 -0
  17. package/dist/server.cjs +73 -0
  18. package/dist/server.d.cts +46 -0
  19. package/dist/server.d.ts +46 -0
  20. package/dist/server.js +49 -0
  21. package/dist/style-downloader.cjs +314 -0
  22. package/dist/style-downloader.d.cts +119 -0
  23. package/dist/style-downloader.d.ts +119 -0
  24. package/dist/style-downloader.js +290 -0
  25. package/dist/tile-downloader.cjs +156 -0
  26. package/dist/tile-downloader.d.cts +83 -0
  27. package/dist/tile-downloader.d.ts +83 -0
  28. package/dist/tile-downloader.js +124 -0
  29. package/dist/types-CJq90eOB.d.cts +184 -0
  30. package/dist/types-CJq90eOB.d.ts +184 -0
  31. package/dist/utils/errors.cjs +41 -0
  32. package/dist/utils/errors.d.cts +18 -0
  33. package/dist/utils/errors.d.ts +18 -0
  34. package/dist/utils/errors.js +16 -0
  35. package/dist/utils/fetch.cjs +97 -0
  36. package/dist/utils/fetch.d.cts +50 -0
  37. package/dist/utils/fetch.d.ts +50 -0
  38. package/dist/utils/fetch.js +63 -0
  39. package/dist/utils/file-formats.cjs +96 -0
  40. package/dist/utils/file-formats.d.cts +33 -0
  41. package/dist/utils/file-formats.d.ts +33 -0
  42. package/dist/utils/file-formats.js +70 -0
  43. package/dist/utils/geo.cjs +84 -0
  44. package/dist/utils/geo.d.cts +46 -0
  45. package/dist/utils/geo.d.ts +46 -0
  46. package/dist/utils/geo.js +56 -0
  47. package/dist/utils/mapbox.cjs +121 -0
  48. package/dist/utils/mapbox.d.cts +43 -0
  49. package/dist/utils/mapbox.d.ts +43 -0
  50. package/dist/utils/mapbox.js +91 -0
  51. package/dist/utils/misc.cjs +39 -0
  52. package/dist/utils/misc.d.cts +22 -0
  53. package/dist/utils/misc.d.ts +22 -0
  54. package/dist/utils/misc.js +13 -0
  55. package/dist/utils/streams.cjs +99 -0
  56. package/dist/utils/streams.d.cts +49 -0
  57. package/dist/utils/streams.d.ts +49 -0
  58. package/dist/utils/streams.js +73 -0
  59. package/dist/utils/style.cjs +126 -0
  60. package/dist/utils/style.d.cts +67 -0
  61. package/dist/utils/style.d.ts +67 -0
  62. package/dist/utils/style.js +98 -0
  63. package/dist/utils/templates.cjs +124 -0
  64. package/dist/utils/templates.d.cts +80 -0
  65. package/dist/utils/templates.d.ts +80 -0
  66. package/dist/utils/templates.js +85 -0
  67. package/dist/writer.cjs +465 -0
  68. package/dist/writer.d.cts +5 -0
  69. package/dist/writer.d.ts +5 -0
  70. package/dist/writer.js +452 -0
  71. package/package.json +161 -0
@@ -0,0 +1,287 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var reader_exports = {};
30
+ __export(reader_exports, {
31
+ Reader: () => Reader
32
+ });
33
+ module.exports = __toCommonJS(reader_exports);
34
+ var import_zip_reader = require("@gmaclennan/zip-reader");
35
+ var import_errors = require('./utils/errors.cjs');
36
+ var import_misc = require('./utils/misc.cjs');
37
+ var import_style = require('./utils/style.cjs');
38
+ var import_templates = require('./utils/templates.cjs');
39
+ function defer() {
40
+ let resolve;
41
+ let reject;
42
+ const promise = new Promise((res, rej) => {
43
+ resolve = res;
44
+ reject = rej;
45
+ });
46
+ return { promise, resolve, reject };
47
+ }
48
+ class Entries {
49
+ /** @type {Map<string, import('@gmaclennan/zip-reader').ZipEntry>} */
50
+ #entries = /* @__PURE__ */ new Map();
51
+ /** @type {Map<string, ReturnType<typeof defer<import('@gmaclennan/zip-reader').ZipEntry | undefined>>>} */
52
+ #deferredEntries = /* @__PURE__ */ new Map();
53
+ #readyPromise;
54
+ #ready = false;
55
+ #closing = false;
56
+ /** @type {unknown} */
57
+ #error;
58
+ /** @param {Promise<import('@gmaclennan/zip-reader').ZipReader>} zipPromise */
59
+ constructor(zipPromise) {
60
+ this.#readyPromise = (async () => {
61
+ try {
62
+ if (this.#closing) return;
63
+ const zip = await zipPromise;
64
+ if (this.#closing) return;
65
+ for await (const entry of zip) {
66
+ if (this.#closing) return;
67
+ this.#entries.set(entry.name, entry);
68
+ this.#deferredEntries.get(entry.name)?.resolve(entry);
69
+ this.#deferredEntries.delete(entry.name);
70
+ }
71
+ } catch (error) {
72
+ this.#error = error;
73
+ for (const deferred of this.#deferredEntries.values()) {
74
+ deferred.reject(error);
75
+ }
76
+ this.#deferredEntries.clear();
77
+ throw error;
78
+ } finally {
79
+ this.#ready = true;
80
+ for (const deferred of this.#deferredEntries.values()) {
81
+ deferred.resolve(void 0);
82
+ }
83
+ this.#deferredEntries.clear();
84
+ }
85
+ })();
86
+ this.#readyPromise.catch(import_misc.noop);
87
+ }
88
+ async ready() {
89
+ await this.#readyPromise;
90
+ }
91
+ /** @param {string} path */
92
+ async get(path) {
93
+ if (this.#entries.has(path)) {
94
+ return this.#entries.get(path);
95
+ }
96
+ if (this.#ready || this.#closing) {
97
+ if (this.#error) throw this.#error;
98
+ return void 0;
99
+ }
100
+ const existingDeferred = this.#deferredEntries.get(path);
101
+ if (existingDeferred) {
102
+ return existingDeferred.promise;
103
+ }
104
+ const deferred = defer();
105
+ this.#deferredEntries.set(path, deferred);
106
+ return deferred.promise;
107
+ }
108
+ async close() {
109
+ this.#closing = true;
110
+ await this.#readyPromise;
111
+ }
112
+ }
113
+ async function streamToText(readable) {
114
+ const chunks = (
115
+ /** @type {Uint8Array[]} */
116
+ []
117
+ );
118
+ const reader = readable.getReader();
119
+ try {
120
+ while (true) {
121
+ const { done, value } = await reader.read();
122
+ if (done) break;
123
+ chunks.push(value instanceof Uint8Array ? value : new Uint8Array(value));
124
+ }
125
+ } finally {
126
+ reader.releaseLock();
127
+ }
128
+ const totalLen = chunks.reduce((s, c) => s + c.byteLength, 0);
129
+ const buf = new Uint8Array(totalLen);
130
+ let off = 0;
131
+ for (const chunk of chunks) {
132
+ buf.set(chunk, off);
133
+ off += chunk.byteLength;
134
+ }
135
+ return new TextDecoder().decode(buf);
136
+ }
137
+ async function streamToJson(readable) {
138
+ return JSON.parse(await streamToText(readable));
139
+ }
140
+ class Reader {
141
+ #entries;
142
+ /** @type {undefined | Promise<void>} */
143
+ #closePromise;
144
+ /** @type {import('@gmaclennan/zip-reader/file-source').FileSource | null} */
145
+ #fileSource = null;
146
+ /**
147
+ * @param {string | import('@gmaclennan/zip-reader').ZipReader} filepathOrZip Path to styled map package (`.styledmap`) file, or a ZipReader instance
148
+ */
149
+ constructor(filepathOrZip) {
150
+ let zipPromise;
151
+ if (typeof filepathOrZip === "string") {
152
+ const sourcePromise = import("@gmaclennan/zip-reader/file-source").then(
153
+ ({ FileSource }) => FileSource.open(filepathOrZip)
154
+ );
155
+ sourcePromise.catch(import_misc.noop);
156
+ zipPromise = sourcePromise.then((source) => {
157
+ this.#fileSource = source;
158
+ return import_zip_reader.ZipReader.from(source);
159
+ });
160
+ } else {
161
+ zipPromise = Promise.resolve(filepathOrZip);
162
+ }
163
+ zipPromise.catch(import_misc.noop);
164
+ this.#entries = new Entries(zipPromise);
165
+ this.#entries.ready().catch(() => this.close());
166
+ }
167
+ /**
168
+ * Resolves when the styled map package has been opened and the entries have
169
+ * been read. Throws any error that occurred during opening.
170
+ */
171
+ async opened() {
172
+ await this.#entries.ready();
173
+ }
174
+ /**
175
+ * Get the format version from the VERSION file in the styled map package.
176
+ * Returns "1.0" if no VERSION file exists (older SMP files did not have a
177
+ * VERSION file, so we assume version 1.0).
178
+ *
179
+ * @returns {Promise<string>}
180
+ */
181
+ async getVersion() {
182
+ const versionEntry = await this.#entries.get(import_templates.VERSION_FILE);
183
+ if (!versionEntry) return "1.0";
184
+ return (await streamToText(versionEntry.readable())).trim();
185
+ }
186
+ /**
187
+ * Get the style JSON from the styled map package. The URLs in the style JSON
188
+ * will be transformed to use the provided base URL.
189
+ *
190
+ * @param {string | null} [baseUrl] Base URL where you plan to serve the resources in this styled map package, e.g. `http://localhost:3000/maps/styleA`
191
+ * @returns {Promise<import('./types.js').SMPStyle>}
192
+ */
193
+ async getStyle(baseUrl = null) {
194
+ const styleEntry = await this.#entries.get(import_templates.STYLE_FILE);
195
+ if (!styleEntry) throw new import_errors.ENOENT(import_templates.STYLE_FILE);
196
+ const style = await streamToJson(styleEntry.readable());
197
+ if (!(0, import_style.validateStyle)(style)) {
198
+ throw new AggregateError(import_style.validateStyle.errors, "Invalid style");
199
+ }
200
+ if (typeof style.glyphs === "string") {
201
+ style.glyphs = getUrl(style.glyphs, baseUrl);
202
+ }
203
+ if (typeof style.sprite === "string") {
204
+ style.sprite = getUrl(style.sprite, baseUrl);
205
+ } else if (Array.isArray(style.sprite)) {
206
+ style.sprite = style.sprite.map(({ id, url }) => {
207
+ return { id, url: getUrl(url, baseUrl) };
208
+ });
209
+ }
210
+ for (const source of Object.values(style.sources)) {
211
+ if ("tiles" in source && source.tiles) {
212
+ source.tiles = source.tiles.map((tile) => getUrl(tile, baseUrl));
213
+ }
214
+ }
215
+ return (
216
+ /** @type {import('./types.js').SMPStyle} */
217
+ style
218
+ );
219
+ }
220
+ /**
221
+ * Get a resource from the styled map package. The path should be relative to
222
+ * the root of the package.
223
+ *
224
+ * @param {string} path
225
+ * @returns {Promise<Resource>}
226
+ */
227
+ async getResource(path) {
228
+ if (path[0] === "/") path = path.slice(1);
229
+ if (path === import_templates.STYLE_FILE) {
230
+ const styleJSON = JSON.stringify(await this.getStyle());
231
+ const bytes = new TextEncoder().encode(styleJSON);
232
+ return {
233
+ contentType: "application/json; charset=utf-8",
234
+ contentLength: bytes.byteLength,
235
+ resourceType: "style",
236
+ stream: new ReadableStream({
237
+ start(controller) {
238
+ controller.enqueue(bytes);
239
+ controller.close();
240
+ }
241
+ })
242
+ };
243
+ }
244
+ const entry = await this.#entries.get(path);
245
+ if (!entry) throw new import_errors.ENOENT(path);
246
+ const resourceType = (0, import_templates.getResourceType)(path);
247
+ const contentType = (0, import_templates.getContentType)(path);
248
+ const stream = entry.readable();
249
+ const resource = {
250
+ resourceType,
251
+ contentType,
252
+ contentLength: entry.uncompressedSize,
253
+ stream
254
+ };
255
+ if (path.endsWith(".gz")) {
256
+ resource.contentEncoding = "gzip";
257
+ }
258
+ return resource;
259
+ }
260
+ /**
261
+ * Close the styled map package file (should be called after reading the file to avoid memory leaks)
262
+ */
263
+ async close() {
264
+ if (this.#closePromise) return this.#closePromise;
265
+ this.#closePromise = (async () => {
266
+ await this.#entries.close().catch(import_misc.noop);
267
+ if (this.#fileSource) {
268
+ await this.#fileSource.close().catch(import_misc.noop);
269
+ }
270
+ })();
271
+ return this.#closePromise;
272
+ }
273
+ }
274
+ function getUrl(smpUri, baseUrl) {
275
+ if (!smpUri.startsWith(import_templates.URI_BASE)) {
276
+ throw new Error(`Invalid SMP URI: ${smpUri}`);
277
+ }
278
+ if (typeof baseUrl !== "string") return smpUri;
279
+ if (!baseUrl.endsWith("/")) {
280
+ baseUrl += "/";
281
+ }
282
+ return smpUri.replace(import_templates.URI_BASE, baseUrl);
283
+ }
284
+ // Annotate the CommonJS export names for ESM import in node:
285
+ 0 && (module.exports = {
286
+ Reader
287
+ });
@@ -0,0 +1,68 @@
1
+ import { a as SMPStyle } from './types-CJq90eOB.cjs';
2
+ import * as _gmaclennan_zip_reader from '@gmaclennan/zip-reader';
3
+ import '@maplibre/maplibre-gl-style-spec';
4
+ import 'geojson';
5
+ import 'type-fest';
6
+ import 'events';
7
+
8
+ /**
9
+ * @typedef {object} Resource
10
+ * @property {string} resourceType
11
+ * @property {string} contentType
12
+ * @property {number} contentLength
13
+ * @property {ReadableStream<Uint8Array>} stream
14
+ * @property {'gzip'} [contentEncoding]
15
+ */
16
+ /**
17
+ * A low-level reader for styled map packages. Returns resources in the package
18
+ * as readable streams, for serving over HTTP for example.
19
+ */
20
+ declare class Reader {
21
+ /**
22
+ * @param {string | import('@gmaclennan/zip-reader').ZipReader} filepathOrZip Path to styled map package (`.styledmap`) file, or a ZipReader instance
23
+ */
24
+ constructor(filepathOrZip: string | _gmaclennan_zip_reader.ZipReader);
25
+ /**
26
+ * Resolves when the styled map package has been opened and the entries have
27
+ * been read. Throws any error that occurred during opening.
28
+ */
29
+ opened(): Promise<void>;
30
+ /**
31
+ * Get the format version from the VERSION file in the styled map package.
32
+ * Returns "1.0" if no VERSION file exists (older SMP files did not have a
33
+ * VERSION file, so we assume version 1.0).
34
+ *
35
+ * @returns {Promise<string>}
36
+ */
37
+ getVersion(): Promise<string>;
38
+ /**
39
+ * Get the style JSON from the styled map package. The URLs in the style JSON
40
+ * will be transformed to use the provided base URL.
41
+ *
42
+ * @param {string | null} [baseUrl] Base URL where you plan to serve the resources in this styled map package, e.g. `http://localhost:3000/maps/styleA`
43
+ * @returns {Promise<import('./types.js').SMPStyle>}
44
+ */
45
+ getStyle(baseUrl?: string | null): Promise<SMPStyle>;
46
+ /**
47
+ * Get a resource from the styled map package. The path should be relative to
48
+ * the root of the package.
49
+ *
50
+ * @param {string} path
51
+ * @returns {Promise<Resource>}
52
+ */
53
+ getResource(path: string): Promise<Resource>;
54
+ /**
55
+ * Close the styled map package file (should be called after reading the file to avoid memory leaks)
56
+ */
57
+ close(): Promise<void>;
58
+ #private;
59
+ }
60
+ type Resource = {
61
+ resourceType: string;
62
+ contentType: string;
63
+ contentLength: number;
64
+ stream: ReadableStream<Uint8Array>;
65
+ contentEncoding?: "gzip" | undefined;
66
+ };
67
+
68
+ export { Reader, type Resource };
@@ -0,0 +1,68 @@
1
+ import { a as SMPStyle } from './types-CJq90eOB.js';
2
+ import * as _gmaclennan_zip_reader from '@gmaclennan/zip-reader';
3
+ import '@maplibre/maplibre-gl-style-spec';
4
+ import 'geojson';
5
+ import 'type-fest';
6
+ import 'events';
7
+
8
+ /**
9
+ * @typedef {object} Resource
10
+ * @property {string} resourceType
11
+ * @property {string} contentType
12
+ * @property {number} contentLength
13
+ * @property {ReadableStream<Uint8Array>} stream
14
+ * @property {'gzip'} [contentEncoding]
15
+ */
16
+ /**
17
+ * A low-level reader for styled map packages. Returns resources in the package
18
+ * as readable streams, for serving over HTTP for example.
19
+ */
20
+ declare class Reader {
21
+ /**
22
+ * @param {string | import('@gmaclennan/zip-reader').ZipReader} filepathOrZip Path to styled map package (`.styledmap`) file, or a ZipReader instance
23
+ */
24
+ constructor(filepathOrZip: string | _gmaclennan_zip_reader.ZipReader);
25
+ /**
26
+ * Resolves when the styled map package has been opened and the entries have
27
+ * been read. Throws any error that occurred during opening.
28
+ */
29
+ opened(): Promise<void>;
30
+ /**
31
+ * Get the format version from the VERSION file in the styled map package.
32
+ * Returns "1.0" if no VERSION file exists (older SMP files did not have a
33
+ * VERSION file, so we assume version 1.0).
34
+ *
35
+ * @returns {Promise<string>}
36
+ */
37
+ getVersion(): Promise<string>;
38
+ /**
39
+ * Get the style JSON from the styled map package. The URLs in the style JSON
40
+ * will be transformed to use the provided base URL.
41
+ *
42
+ * @param {string | null} [baseUrl] Base URL where you plan to serve the resources in this styled map package, e.g. `http://localhost:3000/maps/styleA`
43
+ * @returns {Promise<import('./types.js').SMPStyle>}
44
+ */
45
+ getStyle(baseUrl?: string | null): Promise<SMPStyle>;
46
+ /**
47
+ * Get a resource from the styled map package. The path should be relative to
48
+ * the root of the package.
49
+ *
50
+ * @param {string} path
51
+ * @returns {Promise<Resource>}
52
+ */
53
+ getResource(path: string): Promise<Resource>;
54
+ /**
55
+ * Close the styled map package file (should be called after reading the file to avoid memory leaks)
56
+ */
57
+ close(): Promise<void>;
58
+ #private;
59
+ }
60
+ type Resource = {
61
+ resourceType: string;
62
+ contentType: string;
63
+ contentLength: number;
64
+ stream: ReadableStream<Uint8Array>;
65
+ contentEncoding?: "gzip" | undefined;
66
+ };
67
+
68
+ export { Reader, type Resource };
package/dist/reader.js ADDED
@@ -0,0 +1,259 @@
1
+ import { ZipReader } from "@gmaclennan/zip-reader";
2
+ import { ENOENT } from "./utils/errors.js";
3
+ import { noop } from "./utils/misc.js";
4
+ import { validateStyle } from "./utils/style.js";
5
+ import {
6
+ getContentType,
7
+ getResourceType,
8
+ STYLE_FILE,
9
+ URI_BASE,
10
+ VERSION_FILE
11
+ } from "./utils/templates.js";
12
+ function defer() {
13
+ let resolve;
14
+ let reject;
15
+ const promise = new Promise((res, rej) => {
16
+ resolve = res;
17
+ reject = rej;
18
+ });
19
+ return { promise, resolve, reject };
20
+ }
21
+ class Entries {
22
+ /** @type {Map<string, import('@gmaclennan/zip-reader').ZipEntry>} */
23
+ #entries = /* @__PURE__ */ new Map();
24
+ /** @type {Map<string, ReturnType<typeof defer<import('@gmaclennan/zip-reader').ZipEntry | undefined>>>} */
25
+ #deferredEntries = /* @__PURE__ */ new Map();
26
+ #readyPromise;
27
+ #ready = false;
28
+ #closing = false;
29
+ /** @type {unknown} */
30
+ #error;
31
+ /** @param {Promise<import('@gmaclennan/zip-reader').ZipReader>} zipPromise */
32
+ constructor(zipPromise) {
33
+ this.#readyPromise = (async () => {
34
+ try {
35
+ if (this.#closing) return;
36
+ const zip = await zipPromise;
37
+ if (this.#closing) return;
38
+ for await (const entry of zip) {
39
+ if (this.#closing) return;
40
+ this.#entries.set(entry.name, entry);
41
+ this.#deferredEntries.get(entry.name)?.resolve(entry);
42
+ this.#deferredEntries.delete(entry.name);
43
+ }
44
+ } catch (error) {
45
+ this.#error = error;
46
+ for (const deferred of this.#deferredEntries.values()) {
47
+ deferred.reject(error);
48
+ }
49
+ this.#deferredEntries.clear();
50
+ throw error;
51
+ } finally {
52
+ this.#ready = true;
53
+ for (const deferred of this.#deferredEntries.values()) {
54
+ deferred.resolve(void 0);
55
+ }
56
+ this.#deferredEntries.clear();
57
+ }
58
+ })();
59
+ this.#readyPromise.catch(noop);
60
+ }
61
+ async ready() {
62
+ await this.#readyPromise;
63
+ }
64
+ /** @param {string} path */
65
+ async get(path) {
66
+ if (this.#entries.has(path)) {
67
+ return this.#entries.get(path);
68
+ }
69
+ if (this.#ready || this.#closing) {
70
+ if (this.#error) throw this.#error;
71
+ return void 0;
72
+ }
73
+ const existingDeferred = this.#deferredEntries.get(path);
74
+ if (existingDeferred) {
75
+ return existingDeferred.promise;
76
+ }
77
+ const deferred = defer();
78
+ this.#deferredEntries.set(path, deferred);
79
+ return deferred.promise;
80
+ }
81
+ async close() {
82
+ this.#closing = true;
83
+ await this.#readyPromise;
84
+ }
85
+ }
86
+ async function streamToText(readable) {
87
+ const chunks = (
88
+ /** @type {Uint8Array[]} */
89
+ []
90
+ );
91
+ const reader = readable.getReader();
92
+ try {
93
+ while (true) {
94
+ const { done, value } = await reader.read();
95
+ if (done) break;
96
+ chunks.push(value instanceof Uint8Array ? value : new Uint8Array(value));
97
+ }
98
+ } finally {
99
+ reader.releaseLock();
100
+ }
101
+ const totalLen = chunks.reduce((s, c) => s + c.byteLength, 0);
102
+ const buf = new Uint8Array(totalLen);
103
+ let off = 0;
104
+ for (const chunk of chunks) {
105
+ buf.set(chunk, off);
106
+ off += chunk.byteLength;
107
+ }
108
+ return new TextDecoder().decode(buf);
109
+ }
110
+ async function streamToJson(readable) {
111
+ return JSON.parse(await streamToText(readable));
112
+ }
113
+ class Reader {
114
+ #entries;
115
+ /** @type {undefined | Promise<void>} */
116
+ #closePromise;
117
+ /** @type {import('@gmaclennan/zip-reader/file-source').FileSource | null} */
118
+ #fileSource = null;
119
+ /**
120
+ * @param {string | import('@gmaclennan/zip-reader').ZipReader} filepathOrZip Path to styled map package (`.styledmap`) file, or a ZipReader instance
121
+ */
122
+ constructor(filepathOrZip) {
123
+ let zipPromise;
124
+ if (typeof filepathOrZip === "string") {
125
+ const sourcePromise = import("@gmaclennan/zip-reader/file-source").then(
126
+ ({ FileSource }) => FileSource.open(filepathOrZip)
127
+ );
128
+ sourcePromise.catch(noop);
129
+ zipPromise = sourcePromise.then((source) => {
130
+ this.#fileSource = source;
131
+ return ZipReader.from(source);
132
+ });
133
+ } else {
134
+ zipPromise = Promise.resolve(filepathOrZip);
135
+ }
136
+ zipPromise.catch(noop);
137
+ this.#entries = new Entries(zipPromise);
138
+ this.#entries.ready().catch(() => this.close());
139
+ }
140
+ /**
141
+ * Resolves when the styled map package has been opened and the entries have
142
+ * been read. Throws any error that occurred during opening.
143
+ */
144
+ async opened() {
145
+ await this.#entries.ready();
146
+ }
147
+ /**
148
+ * Get the format version from the VERSION file in the styled map package.
149
+ * Returns "1.0" if no VERSION file exists (older SMP files did not have a
150
+ * VERSION file, so we assume version 1.0).
151
+ *
152
+ * @returns {Promise<string>}
153
+ */
154
+ async getVersion() {
155
+ const versionEntry = await this.#entries.get(VERSION_FILE);
156
+ if (!versionEntry) return "1.0";
157
+ return (await streamToText(versionEntry.readable())).trim();
158
+ }
159
+ /**
160
+ * Get the style JSON from the styled map package. The URLs in the style JSON
161
+ * will be transformed to use the provided base URL.
162
+ *
163
+ * @param {string | null} [baseUrl] Base URL where you plan to serve the resources in this styled map package, e.g. `http://localhost:3000/maps/styleA`
164
+ * @returns {Promise<import('./types.js').SMPStyle>}
165
+ */
166
+ async getStyle(baseUrl = null) {
167
+ const styleEntry = await this.#entries.get(STYLE_FILE);
168
+ if (!styleEntry) throw new ENOENT(STYLE_FILE);
169
+ const style = await streamToJson(styleEntry.readable());
170
+ if (!validateStyle(style)) {
171
+ throw new AggregateError(validateStyle.errors, "Invalid style");
172
+ }
173
+ if (typeof style.glyphs === "string") {
174
+ style.glyphs = getUrl(style.glyphs, baseUrl);
175
+ }
176
+ if (typeof style.sprite === "string") {
177
+ style.sprite = getUrl(style.sprite, baseUrl);
178
+ } else if (Array.isArray(style.sprite)) {
179
+ style.sprite = style.sprite.map(({ id, url }) => {
180
+ return { id, url: getUrl(url, baseUrl) };
181
+ });
182
+ }
183
+ for (const source of Object.values(style.sources)) {
184
+ if ("tiles" in source && source.tiles) {
185
+ source.tiles = source.tiles.map((tile) => getUrl(tile, baseUrl));
186
+ }
187
+ }
188
+ return (
189
+ /** @type {import('./types.js').SMPStyle} */
190
+ style
191
+ );
192
+ }
193
+ /**
194
+ * Get a resource from the styled map package. The path should be relative to
195
+ * the root of the package.
196
+ *
197
+ * @param {string} path
198
+ * @returns {Promise<Resource>}
199
+ */
200
+ async getResource(path) {
201
+ if (path[0] === "/") path = path.slice(1);
202
+ if (path === STYLE_FILE) {
203
+ const styleJSON = JSON.stringify(await this.getStyle());
204
+ const bytes = new TextEncoder().encode(styleJSON);
205
+ return {
206
+ contentType: "application/json; charset=utf-8",
207
+ contentLength: bytes.byteLength,
208
+ resourceType: "style",
209
+ stream: new ReadableStream({
210
+ start(controller) {
211
+ controller.enqueue(bytes);
212
+ controller.close();
213
+ }
214
+ })
215
+ };
216
+ }
217
+ const entry = await this.#entries.get(path);
218
+ if (!entry) throw new ENOENT(path);
219
+ const resourceType = getResourceType(path);
220
+ const contentType = getContentType(path);
221
+ const stream = entry.readable();
222
+ const resource = {
223
+ resourceType,
224
+ contentType,
225
+ contentLength: entry.uncompressedSize,
226
+ stream
227
+ };
228
+ if (path.endsWith(".gz")) {
229
+ resource.contentEncoding = "gzip";
230
+ }
231
+ return resource;
232
+ }
233
+ /**
234
+ * Close the styled map package file (should be called after reading the file to avoid memory leaks)
235
+ */
236
+ async close() {
237
+ if (this.#closePromise) return this.#closePromise;
238
+ this.#closePromise = (async () => {
239
+ await this.#entries.close().catch(noop);
240
+ if (this.#fileSource) {
241
+ await this.#fileSource.close().catch(noop);
242
+ }
243
+ })();
244
+ return this.#closePromise;
245
+ }
246
+ }
247
+ function getUrl(smpUri, baseUrl) {
248
+ if (!smpUri.startsWith(URI_BASE)) {
249
+ throw new Error(`Invalid SMP URI: ${smpUri}`);
250
+ }
251
+ if (typeof baseUrl !== "string") return smpUri;
252
+ if (!baseUrl.endsWith("/")) {
253
+ baseUrl += "/";
254
+ }
255
+ return smpUri.replace(URI_BASE, baseUrl);
256
+ }
257
+ export {
258
+ Reader
259
+ };