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.
- package/README.md +94 -0
- package/dist/download.d.ts +11 -21
- package/dist/fallbacks.d.ts +32 -0
- package/dist/from-mbtiles.d.ts +1 -3
- package/dist/index.d.ts +11 -24
- package/dist/reader.d.ts +28 -12
- package/dist/server.d.ts +23 -14
- package/dist/style-downloader.d.ts +13 -19
- package/dist/tile-downloader.d.ts +13 -23
- package/dist/types.d.ts +61 -0
- package/dist/utils/errors.d.ts +2 -4
- package/dist/utils/fetch.d.ts +3 -8
- package/dist/utils/file-formats.d.ts +3 -10
- package/dist/utils/geo.d.ts +17 -9
- package/dist/utils/mapbox.d.ts +8 -10
- package/dist/utils/misc.d.ts +3 -5
- package/dist/utils/streams.d.ts +6 -10
- package/dist/utils/style.d.ts +27 -16
- package/dist/utils/templates.d.ts +30 -25
- package/dist/validator.d.ts +66 -0
- package/dist/writer.d.ts +157 -4
- package/lib/download.js +125 -0
- package/lib/fallbacks.js +157 -0
- package/lib/from-mbtiles.js +131 -0
- package/lib/index.js +12 -0
- package/lib/reader.js +360 -0
- package/lib/server.js +222 -0
- package/lib/style-downloader.js +369 -0
- package/lib/tile-downloader.js +189 -0
- package/lib/types.ts +99 -0
- package/lib/utils/errors.js +24 -0
- package/lib/utils/fetch.js +104 -0
- package/lib/utils/file-formats.js +92 -0
- package/lib/utils/geo.js +97 -0
- package/lib/utils/mapbox.js +155 -0
- package/{dist/utils/misc.d.cts → lib/utils/misc.js} +9 -5
- package/lib/utils/streams.js +101 -0
- package/lib/utils/style.js +206 -0
- package/lib/utils/templates.js +165 -0
- package/lib/validator.js +789 -0
- package/lib/writer.js +652 -0
- package/package.json +30 -78
- package/dist/download.cjs +0 -100
- package/dist/download.d.cts +0 -63
- package/dist/download.js +0 -76
- package/dist/from-mbtiles.cjs +0 -108
- package/dist/from-mbtiles.d.cts +0 -14
- package/dist/from-mbtiles.js +0 -84
- package/dist/index.cjs +0 -46
- package/dist/index.d.cts +0 -24
- package/dist/index.js +0 -16
- package/dist/reader.cjs +0 -287
- package/dist/reader.d.cts +0 -67
- package/dist/reader.js +0 -259
- package/dist/server.cjs +0 -73
- package/dist/server.d.cts +0 -45
- package/dist/server.js +0 -49
- package/dist/style-downloader.cjs +0 -314
- package/dist/style-downloader.d.cts +0 -118
- package/dist/style-downloader.js +0 -290
- package/dist/tile-downloader.cjs +0 -156
- package/dist/tile-downloader.d.cts +0 -82
- package/dist/tile-downloader.js +0 -124
- package/dist/types-qfyJk4ot.d.cts +0 -200
- package/dist/types-qfyJk4ot.d.ts +0 -200
- package/dist/utils/errors.cjs +0 -41
- package/dist/utils/errors.d.cts +0 -18
- package/dist/utils/errors.js +0 -16
- package/dist/utils/fetch.cjs +0 -97
- package/dist/utils/fetch.d.cts +0 -50
- package/dist/utils/fetch.js +0 -63
- package/dist/utils/file-formats.cjs +0 -96
- package/dist/utils/file-formats.d.cts +0 -32
- package/dist/utils/file-formats.js +0 -70
- package/dist/utils/geo.cjs +0 -84
- package/dist/utils/geo.d.cts +0 -46
- package/dist/utils/geo.js +0 -56
- package/dist/utils/mapbox.cjs +0 -121
- package/dist/utils/mapbox.d.cts +0 -43
- package/dist/utils/mapbox.js +0 -91
- package/dist/utils/misc.cjs +0 -39
- package/dist/utils/misc.js +0 -13
- package/dist/utils/streams.cjs +0 -99
- package/dist/utils/streams.d.cts +0 -49
- package/dist/utils/streams.js +0 -73
- package/dist/utils/style.cjs +0 -126
- package/dist/utils/style.d.cts +0 -66
- package/dist/utils/style.js +0 -98
- package/dist/utils/templates.cjs +0 -124
- package/dist/utils/templates.d.cts +0 -79
- package/dist/utils/templates.js +0 -85
- package/dist/writer.cjs +0 -539
- package/dist/writer.d.cts +0 -4
- package/dist/writer.js +0 -516
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import ky from 'ky'
|
|
2
|
+
import pLimit from 'p-limit'
|
|
3
|
+
|
|
4
|
+
import { ProgressStream } from './streams.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} DownloadResponse
|
|
8
|
+
* @property {ReadableStream<Uint8Array>} body Web ReadableStream of the response body
|
|
9
|
+
* @property {string | null} mimeType Content mime-type (from http content-type header)
|
|
10
|
+
* @property {number | null} contentLength Content length in bytes (from http content-length header)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A wrapper for fetch that limits the number of concurrent downloads.
|
|
15
|
+
*/
|
|
16
|
+
export class FetchQueue {
|
|
17
|
+
/** @type {import('p-limit').LimitFunction} */
|
|
18
|
+
#limit
|
|
19
|
+
/** @param {number} concurrency */
|
|
20
|
+
constructor(concurrency) {
|
|
21
|
+
this.#limit = pLimit(concurrency)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get activeCount() {
|
|
25
|
+
return this.#limit.activeCount
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Fetch a URL, limiting the number of concurrent downloads. Resolves with a
|
|
30
|
+
* `DownloadResponse`, which is a parsed from the Fetch `Response` objects,
|
|
31
|
+
* with `body` as a web ReadableStream, and the MIME type and content length
|
|
32
|
+
* of the response.
|
|
33
|
+
*
|
|
34
|
+
* NB: The response body stream must be consumed to the end, otherwise the
|
|
35
|
+
* queue will never be emptied.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} url
|
|
38
|
+
* @param {{ onprogress?: import('./streams.js').ProgressCallback }} opts
|
|
39
|
+
* @returns {Promise<DownloadResponse>}
|
|
40
|
+
*/
|
|
41
|
+
fetch(url, { onprogress } = {}) {
|
|
42
|
+
// This is wrapped like this so that pLimit limits concurrent `fetchStream`
|
|
43
|
+
// calls, which only resolve when the body is completely downloaded, but
|
|
44
|
+
// this method will return a response as soon as it is available. NB: If the
|
|
45
|
+
// body of a response is never "consumed" (e.g. by reading it to the end),
|
|
46
|
+
// the fetchStream function will never resolve, and the limit will never be
|
|
47
|
+
// released.
|
|
48
|
+
return new Promise((resolveResponse, rejectResponse) => {
|
|
49
|
+
this.#limit(fetchStream, {
|
|
50
|
+
url,
|
|
51
|
+
onresponse: resolveResponse,
|
|
52
|
+
onerror: rejectResponse,
|
|
53
|
+
onprogress,
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* This will resolve when the download is complete, regardless of success or
|
|
61
|
+
* failure, but a readable stream is available before download via the
|
|
62
|
+
* onReadStream param. This strange function signature is used for limiting the
|
|
63
|
+
* number of simultaneous downloads, but still being able to expose the Response
|
|
64
|
+
* as soon as it is available. This is implmented this way to avoid creating
|
|
65
|
+
* unnecessary closures, which is important here because we can have thousands
|
|
66
|
+
* of tile requests.
|
|
67
|
+
*
|
|
68
|
+
* @param {object} opts
|
|
69
|
+
* @param {string} opts.url
|
|
70
|
+
* @param {(response: DownloadResponse) => void} opts.onresponse
|
|
71
|
+
* @param {(err: Error) => void} opts.onerror
|
|
72
|
+
* @param {import('./streams.js').ProgressCallback} [opts.onprogress]
|
|
73
|
+
* @returns {Promise<void>}
|
|
74
|
+
*/
|
|
75
|
+
async function fetchStream({ url, onresponse, onerror, onprogress }) {
|
|
76
|
+
try {
|
|
77
|
+
const response = await ky(url, { retry: 3 })
|
|
78
|
+
if (!response.body) {
|
|
79
|
+
throw new Error('No body in response')
|
|
80
|
+
}
|
|
81
|
+
const contentType = response.headers.get('content-type')
|
|
82
|
+
const mimeType =
|
|
83
|
+
typeof contentType === 'string' ? contentType.split(';')[0] : null
|
|
84
|
+
const contentLengthHeader = response.headers.get('content-length')
|
|
85
|
+
const contentLength =
|
|
86
|
+
contentLengthHeader === null ? null : parseInt(contentLengthHeader, 10)
|
|
87
|
+
|
|
88
|
+
const passthrough = new TransformStream()
|
|
89
|
+
// pipeTo resolves when the body is fully consumed (respects backpressure),
|
|
90
|
+
// which releases the pLimit slot for the next download
|
|
91
|
+
const pipePromise = response.body.pipeTo(passthrough.writable)
|
|
92
|
+
|
|
93
|
+
let body = passthrough.readable
|
|
94
|
+
if (onprogress) {
|
|
95
|
+
const progress = new ProgressStream({ onprogress })
|
|
96
|
+
body = body.pipeThrough(progress)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
onresponse({ body, mimeType, contentLength })
|
|
100
|
+
await pipePromise
|
|
101
|
+
} catch (err) {
|
|
102
|
+
onerror(err instanceof Error ? err : new Error('Unknown error'))
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { hasOwn } from './misc.js'
|
|
2
|
+
|
|
3
|
+
const MAGIC_BYTES = /** @type {const} */ ({
|
|
4
|
+
png: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
|
|
5
|
+
jpg: [0xff, 0xd8, 0xff],
|
|
6
|
+
// eslint-disable-next-line no-sparse-arrays
|
|
7
|
+
webp: [0x52, 0x49, 0x46, 0x46, , , , , 0x57, 0x45, 0x42, 0x50],
|
|
8
|
+
// Include the compression-type byte, which is always 0x08 (DEFLATE) for gzip
|
|
9
|
+
gz: [0x1f, 0x8b, 0x08],
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const MIME_TYPES = /** @type {const} */ ({
|
|
13
|
+
'image/png': 'png',
|
|
14
|
+
'image/jpeg': 'jpg',
|
|
15
|
+
'image/webp': 'webp',
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
/** @type {Map<number, keyof typeof MAGIC_BYTES>} */
|
|
19
|
+
const magicByteMap = new Map()
|
|
20
|
+
for (const [ext, bytes] of Object.entries(MAGIC_BYTES)) {
|
|
21
|
+
magicByteMap.set(
|
|
22
|
+
bytes[0],
|
|
23
|
+
// @ts-ignore
|
|
24
|
+
ext,
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* For a given buffer, determine the tile format based on the magic bytes.
|
|
29
|
+
* Will throw for unknown file types.
|
|
30
|
+
* Smaller and faster version of magic-bytes.js due to the limited use case.
|
|
31
|
+
*
|
|
32
|
+
* @param {Buffer | Uint8Array} buf
|
|
33
|
+
* @returns {import("../writer.js").TileFormat}
|
|
34
|
+
*/
|
|
35
|
+
export function getTileFormatFromBuffer(buf) {
|
|
36
|
+
const ext = magicByteMap.get(buf[0])
|
|
37
|
+
if (!ext) {
|
|
38
|
+
throw new Error('Unknown file type')
|
|
39
|
+
}
|
|
40
|
+
const sig = MAGIC_BYTES[ext]
|
|
41
|
+
for (let i = 1; i < sig.length; i++) {
|
|
42
|
+
if (typeof sig[i] !== 'undefined' && sig[i] !== buf[i]) {
|
|
43
|
+
throw new Error('Unknown file type')
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (ext === 'gz') {
|
|
47
|
+
// Gzipped tiles are always MVT
|
|
48
|
+
return 'mvt'
|
|
49
|
+
}
|
|
50
|
+
return ext
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Determine the tile format from a readable stream from the magic bytes at the
|
|
55
|
+
* start of the file. Used if data is served without a content-type header.
|
|
56
|
+
* Returns the format and a new readable stream that includes all original data.
|
|
57
|
+
*
|
|
58
|
+
* @param {ReadableStream<Uint8Array>} tileData Web ReadableStream
|
|
59
|
+
* @returns {Promise<[import("../writer.js").TileFormat, ReadableStream<Uint8Array>]>}
|
|
60
|
+
*/
|
|
61
|
+
export async function getTileFormatFromStream(tileData) {
|
|
62
|
+
const [stream1, stream2] = tileData.tee()
|
|
63
|
+
|
|
64
|
+
// Read enough bytes to detect the format from the first branch
|
|
65
|
+
// 12 bytes is enough for all magic byte signatures (WEBP needs 12)
|
|
66
|
+
const reader = stream1.getReader()
|
|
67
|
+
let buffer = new Uint8Array(0)
|
|
68
|
+
while (buffer.length < 12) {
|
|
69
|
+
const { done, value } = await reader.read()
|
|
70
|
+
if (done) break
|
|
71
|
+
const newBuffer = new Uint8Array(buffer.length + value.length)
|
|
72
|
+
newBuffer.set(buffer)
|
|
73
|
+
newBuffer.set(value, buffer.length)
|
|
74
|
+
buffer = newBuffer
|
|
75
|
+
}
|
|
76
|
+
reader.cancel()
|
|
77
|
+
|
|
78
|
+
const format = getTileFormatFromBuffer(buffer)
|
|
79
|
+
return [format, stream2]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get the tile format from a MIME type. Throws for unsupported types.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} mimeType
|
|
86
|
+
* @returns {import("../writer.js").TileFormat}
|
|
87
|
+
*/
|
|
88
|
+
export function getFormatFromMimeType(mimeType) {
|
|
89
|
+
if (mimeType.startsWith('application/')) return 'mvt'
|
|
90
|
+
if (hasOwn(MIME_TYPES, mimeType)) return MIME_TYPES[mimeType]
|
|
91
|
+
throw new Error('Unsupported MIME type ' + mimeType)
|
|
92
|
+
}
|
package/lib/utils/geo.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Adapted from https://github.com/mapbox/tilebelt
|
|
2
|
+
|
|
3
|
+
const r2d = 180 / Math.PI
|
|
4
|
+
|
|
5
|
+
/** Spherical Mercator max bounds, rounded to 6 decimal places */
|
|
6
|
+
export const MAX_BOUNDS = /** @type {BBox} */ ([
|
|
7
|
+
-180, -85.051129, 180, 85.051129,
|
|
8
|
+
])
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {[number, number, number, number]} BBox
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Return the bounding box for the given tile.
|
|
16
|
+
*
|
|
17
|
+
* @param {{ x: number, y: number, z: number }} tile
|
|
18
|
+
* @returns {BBox} Bounding Box [w, s, e, n]
|
|
19
|
+
*/
|
|
20
|
+
export function tileToBBox({ x, y, z }) {
|
|
21
|
+
const e = tile2lon({ x: x + 1, z })
|
|
22
|
+
const w = tile2lon({ x, z })
|
|
23
|
+
const s = tile2lat({ y: y + 1, z })
|
|
24
|
+
const n = tile2lat({ y, z })
|
|
25
|
+
return [w, s, e, n]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {{ x: number, y: number, z: number }} tile
|
|
30
|
+
*/
|
|
31
|
+
export function getQuadkey({ x, y, z }) {
|
|
32
|
+
let quadkey = ''
|
|
33
|
+
let mask
|
|
34
|
+
for (let i = z; i > 0; i--) {
|
|
35
|
+
mask = 1 << (i - 1)
|
|
36
|
+
quadkey += (x & mask ? 1 : 0) + (y & mask ? 2 : 0)
|
|
37
|
+
}
|
|
38
|
+
return quadkey
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* From an array of tile URL templates, get the URL for the given tile.
|
|
43
|
+
*
|
|
44
|
+
* @param {string[]} urls
|
|
45
|
+
* @param {{ x: number, y: number, z: number, scheme?: 'xyz' | 'tms' }} opts
|
|
46
|
+
*/
|
|
47
|
+
export function getTileUrl(urls, { x, y, z, scheme = 'xyz' }) {
|
|
48
|
+
const bboxEspg3857 = tileToBBox({ x, y: Math.pow(2, z) - y - 1, z })
|
|
49
|
+
const quadkey = getQuadkey({ x, y, z })
|
|
50
|
+
|
|
51
|
+
return urls[(x + y) % urls.length]
|
|
52
|
+
.replace('{prefix}', (x % 16).toString(16) + (y % 16).toString(16))
|
|
53
|
+
.replace(/{z}/g, String(z))
|
|
54
|
+
.replace(/{x}/g, String(x))
|
|
55
|
+
.replace(/{y}/g, String(scheme === 'tms' ? Math.pow(2, z) - y - 1 : y))
|
|
56
|
+
.replace('{quadkey}', quadkey)
|
|
57
|
+
.replace('{bbox-epsg-3857}', bboxEspg3857.join(','))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Returns a bbox that is the smallest bounding box that contains all the input bboxes.
|
|
62
|
+
*
|
|
63
|
+
* @param {[Readonly<BBox>, ...Readonly<BBox>[]]} bboxes
|
|
64
|
+
* @returns {BBox} Bounding Box [w, s, e, n]
|
|
65
|
+
*/
|
|
66
|
+
export function unionBBox(bboxes) {
|
|
67
|
+
let [w, s, e, n] = bboxes[0]
|
|
68
|
+
for (let i = 1; i < bboxes.length; i++) {
|
|
69
|
+
const [w1, s1, e1, n1] = bboxes[i]
|
|
70
|
+
w = Math.min(w, w1)
|
|
71
|
+
s = Math.min(s, s1)
|
|
72
|
+
e = Math.max(e, e1)
|
|
73
|
+
n = Math.max(n, n1)
|
|
74
|
+
}
|
|
75
|
+
return [w, s, e, n]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Convert a TMS Y coordinate to an XYZ Y coordinate.
|
|
80
|
+
*
|
|
81
|
+
* @param {{ y: number, z: number }} tile
|
|
82
|
+
* @returns {number} The XYZ Y coordinate
|
|
83
|
+
*/
|
|
84
|
+
export function tmsToXyzY({ y, z }) {
|
|
85
|
+
return Math.pow(2, z) - y - 1
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** @param {{ x: number, z: number }} opts */
|
|
89
|
+
function tile2lon({ x, z }) {
|
|
90
|
+
return (x / Math.pow(2, z)) * 360 - 180
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** @param {{ y: number, z: number }} opts */
|
|
94
|
+
function tile2lat({ y, z }) {
|
|
95
|
+
const n = Math.PI - (2 * Math.PI * y) / Math.pow(2, z)
|
|
96
|
+
return r2d * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)))
|
|
97
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// from https://github.com/mapbox/mapbox-gl-js/blob/495a695/src/util/mapbox.js
|
|
3
|
+
|
|
4
|
+
export const API_URL = 'https://api.mapbox.com'
|
|
5
|
+
const HELP = 'See https://www.mapbox.com/api-documentation/#access-tokens'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object} URLObject
|
|
9
|
+
* @property {string} protocol
|
|
10
|
+
* @property {string} authority
|
|
11
|
+
* @property {string} path
|
|
12
|
+
* @property {string[]} params
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {URLObject} urlObject
|
|
17
|
+
* @param {string} [accessToken]
|
|
18
|
+
*/
|
|
19
|
+
function makeAPIURL(urlObject, accessToken) {
|
|
20
|
+
const apiUrlObject = parseUrl(API_URL)
|
|
21
|
+
urlObject.protocol = apiUrlObject.protocol
|
|
22
|
+
urlObject.authority = apiUrlObject.authority
|
|
23
|
+
|
|
24
|
+
if (!accessToken) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`An API access token is required to use a Mapbox style. ${HELP}`,
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
if (accessToken[0] === 's') {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Use a public access token (pk.*) not a secret access token (sk.*). ${HELP}`,
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
urlObject.params.push(`access_token=${accessToken}`)
|
|
36
|
+
return formatUrl(urlObject)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @param {string} url */
|
|
40
|
+
export function isMapboxURL(url) {
|
|
41
|
+
return url.indexOf('mapbox:') === 0
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} url
|
|
46
|
+
* @param {string} [accessToken]
|
|
47
|
+
*/
|
|
48
|
+
export function normalizeStyleURL(url, accessToken) {
|
|
49
|
+
if (!isMapboxURL(url)) return url
|
|
50
|
+
if (!accessToken) throw new Error('Mapbox styles require an access token')
|
|
51
|
+
const urlObject = parseUrl(url)
|
|
52
|
+
urlObject.path = `/styles/v1${urlObject.path}`
|
|
53
|
+
return makeAPIURL(urlObject, accessToken)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} url
|
|
58
|
+
* @param {string} [accessToken]
|
|
59
|
+
*/
|
|
60
|
+
export function normalizeGlyphsURL(url, accessToken) {
|
|
61
|
+
if (!isMapboxURL(url)) return url
|
|
62
|
+
if (!accessToken) throw new Error('Mapbox styles require an access token')
|
|
63
|
+
const urlObject = parseUrl(url)
|
|
64
|
+
urlObject.path = `/fonts/v1${urlObject.path}`
|
|
65
|
+
return makeAPIURL(urlObject, accessToken)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {string} url
|
|
70
|
+
* @param {string} [accessToken]
|
|
71
|
+
*/
|
|
72
|
+
export function normalizeSourceURL(url, accessToken) {
|
|
73
|
+
if (!isMapboxURL(url)) return url
|
|
74
|
+
if (!accessToken) throw new Error('Mapbox styles require an access token')
|
|
75
|
+
const urlObject = parseUrl(url)
|
|
76
|
+
urlObject.path = `/v4/${urlObject.authority}.json`
|
|
77
|
+
// TileJSON requests need a secure flag appended to their URLs so
|
|
78
|
+
// that the server knows to send SSL-ified resource references.
|
|
79
|
+
urlObject.params.push('secure')
|
|
80
|
+
return makeAPIURL(urlObject, accessToken)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {string} url
|
|
85
|
+
* @param {'' | '@2x'} format
|
|
86
|
+
* @param {'.png' | '.json'} extension
|
|
87
|
+
* @param {string} [accessToken]
|
|
88
|
+
*/
|
|
89
|
+
export function normalizeSpriteURL(url, format, extension, accessToken) {
|
|
90
|
+
const urlObject = parseUrl(url)
|
|
91
|
+
if (!isMapboxURL(url)) {
|
|
92
|
+
urlObject.path += `${format}${extension}`
|
|
93
|
+
return formatUrl(urlObject)
|
|
94
|
+
}
|
|
95
|
+
urlObject.path = `/styles/v1${urlObject.path}/sprite${format}${extension}`
|
|
96
|
+
return makeAPIURL(urlObject, accessToken)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const imageExtensionRe = /(\.(png|jpg)\d*)(?=$)/
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {any} tileURL
|
|
103
|
+
* @param {string} sourceURL
|
|
104
|
+
* @param {256 | 512} [tileSize]
|
|
105
|
+
* @param {{ devicePixelRatio?: number; supportsWebp?: boolean; }} [opts]
|
|
106
|
+
*/
|
|
107
|
+
export function normalizeTileURL(
|
|
108
|
+
tileURL,
|
|
109
|
+
sourceURL,
|
|
110
|
+
tileSize,
|
|
111
|
+
{ devicePixelRatio = 1, supportsWebp = false } = {},
|
|
112
|
+
) {
|
|
113
|
+
if (!sourceURL || !isMapboxURL(sourceURL)) return tileURL
|
|
114
|
+
|
|
115
|
+
const urlObject = parseUrl(tileURL)
|
|
116
|
+
|
|
117
|
+
// The v4 mapbox tile API supports 512x512 image tiles only when @2x
|
|
118
|
+
// is appended to the tile URL. If `tileSize: 512` is specified for
|
|
119
|
+
// a Mapbox raster source force the @2x suffix even if a non hidpi device.
|
|
120
|
+
const suffix = devicePixelRatio >= 2 || tileSize === 512 ? '@2x' : ''
|
|
121
|
+
const extension = supportsWebp ? '.webp' : '$1'
|
|
122
|
+
urlObject.path = urlObject.path.replace(
|
|
123
|
+
imageExtensionRe,
|
|
124
|
+
`${suffix}${extension}`,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return formatUrl(urlObject)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const urlRe = /^(\w+):\/\/([^/?]*)(\/[^?]+)?\??(.+)?/
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @param {string} url
|
|
134
|
+
* @returns {URLObject}
|
|
135
|
+
*/
|
|
136
|
+
function parseUrl(url) {
|
|
137
|
+
const parts = url.match(urlRe)
|
|
138
|
+
if (!parts) {
|
|
139
|
+
throw new Error('Unable to parse URL object')
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
protocol: parts[1],
|
|
143
|
+
authority: parts[2],
|
|
144
|
+
path: parts[3] || '/',
|
|
145
|
+
params: parts[4] ? parts[4].split('&') : [],
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {URLObject} obj
|
|
151
|
+
*/
|
|
152
|
+
function formatUrl(obj) {
|
|
153
|
+
const params = obj.params.length ? `?${obj.params.join('&')}` : ''
|
|
154
|
+
return `${obj.protocol}://${obj.authority}${obj.path}${params}`
|
|
155
|
+
}
|
|
@@ -7,8 +7,12 @@
|
|
|
7
7
|
* @param {T} obj
|
|
8
8
|
* @returns {T}
|
|
9
9
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
export function clone(obj) {
|
|
11
|
+
return JSON.parse(JSON.stringify(obj))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function noop() {}
|
|
15
|
+
|
|
12
16
|
/**
|
|
13
17
|
* Like `Object.hasOwn`, but refines the type of `key`.
|
|
14
18
|
*
|
|
@@ -17,6 +21,6 @@ declare function noop(): void;
|
|
|
17
21
|
* @param {string} key
|
|
18
22
|
* @returns {key is (keyof T)}
|
|
19
23
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
export function hasOwn(obj, key) {
|
|
25
|
+
return Object.hasOwn(obj, key)
|
|
26
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a ReadableStream from an async iterable. Uses the native
|
|
3
|
+
* `ReadableStream.from()` when available (Node 20+), otherwise falls back to a
|
|
4
|
+
* manual approach for Node 18 compatibility.
|
|
5
|
+
*
|
|
6
|
+
* @template T
|
|
7
|
+
* @param {AsyncIterable<T>} iterable
|
|
8
|
+
* @returns {ReadableStream<T>}
|
|
9
|
+
*/
|
|
10
|
+
export function readableFromAsync(iterable) {
|
|
11
|
+
// @ts-ignore - ReadableStream.from() exists in Node 20+ but not in DOM types
|
|
12
|
+
if (typeof ReadableStream.from === 'function') {
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
return ReadableStream.from(iterable)
|
|
15
|
+
}
|
|
16
|
+
const iterator = iterable[Symbol.asyncIterator]()
|
|
17
|
+
return new ReadableStream({
|
|
18
|
+
async pull(controller) {
|
|
19
|
+
const { value, done } = await iterator.next()
|
|
20
|
+
if (done) {
|
|
21
|
+
controller.close()
|
|
22
|
+
} else {
|
|
23
|
+
controller.enqueue(value)
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
async cancel(reason) {
|
|
27
|
+
await iterator.return?.(reason)
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a writable stream from an async function. Default concurrency is 16 -
|
|
34
|
+
* this is the number of parallel functions that will be pending before
|
|
35
|
+
* backpressure is applied on the stream.
|
|
36
|
+
*
|
|
37
|
+
* @template {(...args: any[]) => Promise<void>} T
|
|
38
|
+
* @param {T} fn
|
|
39
|
+
* @returns {WritableStream}
|
|
40
|
+
*/
|
|
41
|
+
export function writeStreamFromAsync(fn, { concurrency = 16 } = {}) {
|
|
42
|
+
const pending = new Set()
|
|
43
|
+
return new WritableStream(
|
|
44
|
+
{
|
|
45
|
+
write(chunk) {
|
|
46
|
+
const p = fn(...chunk)
|
|
47
|
+
pending.add(p)
|
|
48
|
+
p.finally(() => pending.delete(p))
|
|
49
|
+
if (pending.size >= concurrency) {
|
|
50
|
+
return Promise.race(pending)
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
async close() {
|
|
54
|
+
await Promise.all(pending)
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
new CountQueuingStrategy({ highWaterMark: concurrency }),
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** @typedef {(opts: { totalBytes: number, chunkBytes: number }) => void} ProgressCallback */
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A web TransformStream that counts the bytes passing through it. Pass an
|
|
65
|
+
* optional `onprogress` callback that will be called with the accumulated
|
|
66
|
+
* total byte count and the chunk byte count after each chunk.
|
|
67
|
+
*/
|
|
68
|
+
export class ProgressStream {
|
|
69
|
+
#byteLength = 0
|
|
70
|
+
#ts
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {{ onprogress?: ProgressCallback }} [opts]
|
|
74
|
+
*/
|
|
75
|
+
constructor({ onprogress } = {}) {
|
|
76
|
+
const self = this
|
|
77
|
+
this.#ts = new TransformStream({
|
|
78
|
+
transform(chunk, controller) {
|
|
79
|
+
self.#byteLength += chunk.byteLength
|
|
80
|
+
onprogress?.({
|
|
81
|
+
totalBytes: self.#byteLength,
|
|
82
|
+
chunkBytes: chunk.byteLength,
|
|
83
|
+
})
|
|
84
|
+
controller.enqueue(chunk)
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get readable() {
|
|
90
|
+
return this.#ts.readable
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get writable() {
|
|
94
|
+
return this.#ts.writable
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Total bytes that have passed through this stream */
|
|
98
|
+
get byteLength() {
|
|
99
|
+
return this.#byteLength
|
|
100
|
+
}
|
|
101
|
+
}
|