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.
- package/.github/workflows/node.yml +30 -0
- package/.github/workflows/release.yml +47 -0
- package/.husky/pre-commit +1 -0
- package/.nvmrc +1 -0
- package/LICENSE.md +7 -0
- package/README.md +28 -0
- package/bin/smp-download.js +83 -0
- package/bin/smp-view.js +52 -0
- package/bin/smp.js +11 -0
- package/eslint.config.js +17 -0
- package/lib/download.js +114 -0
- package/lib/index.js +6 -0
- package/lib/reader.js +150 -0
- package/lib/reporters.js +92 -0
- package/lib/server.js +64 -0
- package/lib/style-downloader.js +363 -0
- package/lib/tile-downloader.js +188 -0
- package/lib/types.ts +104 -0
- package/lib/utils/fetch.js +100 -0
- package/lib/utils/file-formats.js +85 -0
- package/lib/utils/geo.js +87 -0
- package/lib/utils/mapbox.js +155 -0
- package/lib/utils/misc.js +26 -0
- package/lib/utils/streams.js +162 -0
- package/lib/utils/style.js +174 -0
- package/lib/utils/templates.js +136 -0
- package/lib/writer.js +478 -0
- package/map-viewer/index.html +89 -0
- package/package.json +103 -0
- package/test/download-write-read.js +43 -0
- package/test/fixtures/invalid-styles/empty.json +1 -0
- package/test/fixtures/invalid-styles/missing-source.json +10 -0
- package/test/fixtures/invalid-styles/no-layers.json +4 -0
- package/test/fixtures/invalid-styles/no-sources.json +4 -0
- package/test/fixtures/invalid-styles/null.json +1 -0
- package/test/fixtures/invalid-styles/unsupported-version.json +5 -0
- package/test/fixtures/valid-styles/external-geojson.input.json +66 -0
- package/test/fixtures/valid-styles/external-geojson.output.json +93 -0
- package/test/fixtures/valid-styles/inline-geojson.input.json +421 -0
- package/test/fixtures/valid-styles/inline-geojson.output.json +1573 -0
- package/test/fixtures/valid-styles/maplibre-demotiles.input.json +831 -0
- package/test/fixtures/valid-styles/maplibre-unlabelled.input.json +496 -0
- package/test/fixtures/valid-styles/maplibre-unlabelled.output.json +1573 -0
- package/test/fixtures/valid-styles/minimal-labelled.input.json +37 -0
- package/test/fixtures/valid-styles/minimal-labelled.output.json +72 -0
- package/test/fixtures/valid-styles/minimal-sprites.input.json +37 -0
- package/test/fixtures/valid-styles/minimal-sprites.output.json +58 -0
- package/test/fixtures/valid-styles/minimal.input.json +54 -0
- package/test/fixtures/valid-styles/minimal.output.json +92 -0
- package/test/fixtures/valid-styles/multiple-sprites.input.json +46 -0
- package/test/fixtures/valid-styles/multiple-sprites.output.json +128 -0
- package/test/fixtures/valid-styles/raster-sources.input.json +33 -0
- package/test/fixtures/valid-styles/raster-sources.output.json +69 -0
- package/test/utils/assert-bbox-equal.js +19 -0
- package/test/utils/digest-stream.js +36 -0
- package/test/utils/image-streams.js +30 -0
- package/test/utils/reader-helper.js +72 -0
- package/test/write-read.js +620 -0
- package/tsconfig.json +18 -0
- package/types/buffer-peek-stream.d.ts +12 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Readable, Writable, Transform } from 'readable-stream'
|
|
2
|
+
|
|
3
|
+
/** @import { TransformOptions } from 'readable-stream' */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a writable stream from an async function. Default concurrecy is 16 -
|
|
7
|
+
* this is the number of parallel functions that will be pending before
|
|
8
|
+
* backpressure is applied on the stream.
|
|
9
|
+
*
|
|
10
|
+
* @template {(...args: any[]) => Promise<void>} T
|
|
11
|
+
* @param {T} fn
|
|
12
|
+
* @returns {import('readable-stream').Writable}
|
|
13
|
+
*/
|
|
14
|
+
export function writeStreamFromAsync(fn, { concurrency = 16 } = {}) {
|
|
15
|
+
return new Writable({
|
|
16
|
+
highWaterMark: concurrency,
|
|
17
|
+
objectMode: true,
|
|
18
|
+
write(chunk, encoding, callback) {
|
|
19
|
+
fn.apply(null, chunk).then(() => callback(), callback)
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* From https://github.com/nodejs/node/blob/430c0269/lib/internal/webstreams/adapters.js#L509
|
|
26
|
+
*
|
|
27
|
+
* @param {ReadableStream} readableStream
|
|
28
|
+
* @param {{
|
|
29
|
+
* highWaterMark? : number,
|
|
30
|
+
* encoding? : string,
|
|
31
|
+
* objectMode? : boolean,
|
|
32
|
+
* signal? : AbortSignal,
|
|
33
|
+
* }} [options]
|
|
34
|
+
* @returns {import('stream').Readable}
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
export function fromWebReadableStream(readableStream, options = {}) {
|
|
38
|
+
if (!isWebReadableStream(readableStream)) {
|
|
39
|
+
throw new Error('First argument must be a ReadableStream')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { highWaterMark, encoding, objectMode = false, signal } = options
|
|
43
|
+
|
|
44
|
+
if (encoding !== undefined && !Buffer.isEncoding(encoding))
|
|
45
|
+
throw new Error('Invalid encoding')
|
|
46
|
+
|
|
47
|
+
const reader = readableStream.getReader()
|
|
48
|
+
let closed = false
|
|
49
|
+
|
|
50
|
+
const readable = new Readable({
|
|
51
|
+
objectMode,
|
|
52
|
+
highWaterMark,
|
|
53
|
+
encoding,
|
|
54
|
+
// @ts-ignore
|
|
55
|
+
signal,
|
|
56
|
+
|
|
57
|
+
read() {
|
|
58
|
+
reader.read().then(
|
|
59
|
+
(chunk) => {
|
|
60
|
+
if (chunk.done) {
|
|
61
|
+
// Value should always be undefined here.
|
|
62
|
+
readable.push(null)
|
|
63
|
+
} else {
|
|
64
|
+
readable.push(chunk.value)
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
(error) => readable.destroy(error),
|
|
68
|
+
)
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
destroy(error, callback) {
|
|
72
|
+
function done() {
|
|
73
|
+
try {
|
|
74
|
+
callback(error)
|
|
75
|
+
} catch (error) {
|
|
76
|
+
// In a next tick because this is happening within
|
|
77
|
+
// a promise context, and if there are any errors
|
|
78
|
+
// thrown we don't want those to cause an unhandled
|
|
79
|
+
// rejection. Let's just escape the promise and
|
|
80
|
+
// handle it separately.
|
|
81
|
+
process.nextTick(() => {
|
|
82
|
+
throw error
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!closed) {
|
|
88
|
+
reader.cancel(error).then(done, done)
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
done()
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
reader.closed.then(
|
|
96
|
+
() => {
|
|
97
|
+
closed = true
|
|
98
|
+
},
|
|
99
|
+
(error) => {
|
|
100
|
+
closed = true
|
|
101
|
+
readable.destroy(error)
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return readable
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {unknown} obj
|
|
110
|
+
* @returns {obj is ReadableStream}
|
|
111
|
+
*/
|
|
112
|
+
export function isWebReadableStream(obj) {
|
|
113
|
+
return !!(
|
|
114
|
+
typeof obj === 'object' &&
|
|
115
|
+
obj !== null &&
|
|
116
|
+
'pipeThrough' in obj &&
|
|
117
|
+
typeof obj.pipeThrough === 'function' &&
|
|
118
|
+
'getReader' in obj &&
|
|
119
|
+
typeof obj.getReader === 'function' &&
|
|
120
|
+
'cancel' in obj &&
|
|
121
|
+
typeof obj.cancel === 'function'
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** @typedef {(opts: { totalBytes: number, chunkBytes: number }) => void} ProgressCallback */
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Passthrough stream that counts the bytes passing through it. Pass an optional
|
|
129
|
+
* `onprogress` callback that will be called with the accumulated total byte
|
|
130
|
+
* count and the chunk byte count after each chunk.
|
|
131
|
+
*/
|
|
132
|
+
export class ProgressStream extends Transform {
|
|
133
|
+
#onprogress
|
|
134
|
+
#byteLength = 0
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @param {TransformOptions & { onprogress?: ProgressCallback }} [opts]
|
|
138
|
+
*/
|
|
139
|
+
constructor({ onprogress, ...opts } = {}) {
|
|
140
|
+
super(opts)
|
|
141
|
+
this.#onprogress = onprogress
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Total bytes that have passed through this stream */
|
|
145
|
+
get byteLength() {
|
|
146
|
+
return this.#byteLength
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {Buffer | Uint8Array} chunk
|
|
151
|
+
* @param {Parameters<Transform['_transform']>[1]} encoding
|
|
152
|
+
* @param {Parameters<Transform['_transform']>[2]} callback
|
|
153
|
+
*/
|
|
154
|
+
_transform(chunk, encoding, callback) {
|
|
155
|
+
this.#byteLength += chunk.length
|
|
156
|
+
this.#onprogress?.({
|
|
157
|
+
totalBytes: this.#byteLength,
|
|
158
|
+
chunkBytes: chunk.length,
|
|
159
|
+
})
|
|
160
|
+
callback(null, chunk)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { expressions, validateStyleMin } from '@maplibre/maplibre-gl-style-spec'
|
|
2
|
+
|
|
3
|
+
/** @import {StyleSpecification, ExpressionSpecification, ValidationError} from '@maplibre/maplibre-gl-style-spec' */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* For a given style, replace all font stacks (`text-field` properties) with the
|
|
7
|
+
* provided fonts. If no matching font is found, the first font in the stack is
|
|
8
|
+
* used.
|
|
9
|
+
*
|
|
10
|
+
* *Modifies the input style object*
|
|
11
|
+
*
|
|
12
|
+
* @param {StyleSpecification} style
|
|
13
|
+
* @param {string[]} fonts
|
|
14
|
+
*/
|
|
15
|
+
export function replaceFontStacks(style, fonts) {
|
|
16
|
+
const mappedLayers = mapFontStacks(style.layers, (fontStack) => {
|
|
17
|
+
let match
|
|
18
|
+
for (const font of fontStack) {
|
|
19
|
+
if (fonts.includes(font)) {
|
|
20
|
+
match = font
|
|
21
|
+
break
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return [match || fonts[0]]
|
|
25
|
+
})
|
|
26
|
+
style.layers = mappedLayers
|
|
27
|
+
return style
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* From given style layers, create a new style by calling the provided callback
|
|
32
|
+
* function on every font stack defined in the style.
|
|
33
|
+
*
|
|
34
|
+
* @param {StyleSpecification['layers']} layers
|
|
35
|
+
* @param {(fontStack: string[]) => string[]} callbackFn
|
|
36
|
+
* @returns {StyleSpecification['layers']}
|
|
37
|
+
*/
|
|
38
|
+
export function mapFontStacks(layers, callbackFn) {
|
|
39
|
+
return layers.map((layer) => {
|
|
40
|
+
if (layer.type !== 'symbol' || !layer.layout || !layer.layout['text-font'])
|
|
41
|
+
return layer
|
|
42
|
+
const textFont = layer.layout['text-font']
|
|
43
|
+
let mappedValue
|
|
44
|
+
if (isExpression(textFont)) {
|
|
45
|
+
mappedValue = mapArrayExpressionValue(textFont, callbackFn)
|
|
46
|
+
} else if (Array.isArray(textFont)) {
|
|
47
|
+
mappedValue = callbackFn(textFont)
|
|
48
|
+
} else {
|
|
49
|
+
// Deprecated property function, unsupported, but within this module
|
|
50
|
+
// functions will have been migrated to expressions anyway.
|
|
51
|
+
console.warn(
|
|
52
|
+
'Deprecated function definitions are not supported, font stack has not been transformed.',
|
|
53
|
+
)
|
|
54
|
+
console.dir(textFont, { depth: null })
|
|
55
|
+
return layer
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
...layer,
|
|
59
|
+
layout: {
|
|
60
|
+
...layer.layout,
|
|
61
|
+
'text-font': mappedValue,
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* See https://github.com/maplibre/maplibre-style-spec/blob/c2f01dbaa6c5fb8409126258b9464b450018e939/src/expression/index.ts#L128
|
|
69
|
+
*
|
|
70
|
+
* @param {unknown} value
|
|
71
|
+
* @returns {value is ExpressionSpecification}
|
|
72
|
+
*/
|
|
73
|
+
function isExpression(value) {
|
|
74
|
+
return (
|
|
75
|
+
Array.isArray(value) &&
|
|
76
|
+
value.length > 0 &&
|
|
77
|
+
typeof value[0] === 'string' &&
|
|
78
|
+
value[0] in expressions
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* For an expression whose value is an array, map the array to a new array using
|
|
84
|
+
* the given callbackFn.
|
|
85
|
+
*
|
|
86
|
+
* @param {ExpressionSpecification} expression
|
|
87
|
+
* @param {(value: string[]) => string[]} callbackFn
|
|
88
|
+
* @returns {ExpressionSpecification}
|
|
89
|
+
*/
|
|
90
|
+
function mapArrayExpressionValue(expression, callbackFn) {
|
|
91
|
+
// This only works for properties whose value is an array, because it relies
|
|
92
|
+
// on the style specification that array values must be declared with the
|
|
93
|
+
// `literal` expression.
|
|
94
|
+
if (expression[0] === 'literal' && Array.isArray(expression[1])) {
|
|
95
|
+
return ['literal', callbackFn(expression[1])]
|
|
96
|
+
} else {
|
|
97
|
+
// @ts-ignore
|
|
98
|
+
return [
|
|
99
|
+
expression[0],
|
|
100
|
+
...expression.slice(1).map(
|
|
101
|
+
// @ts-ignore
|
|
102
|
+
(x) => {
|
|
103
|
+
if (isExpression(x)) {
|
|
104
|
+
return mapArrayExpressionValue(x, callbackFn)
|
|
105
|
+
} else {
|
|
106
|
+
return x
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
),
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @typedef {object} TileJSONPartial
|
|
116
|
+
* @property {string[]} tiles
|
|
117
|
+
* @property {string} [description]
|
|
118
|
+
* @property {string} [attribution]
|
|
119
|
+
* @property {object[]} [vector_layers]
|
|
120
|
+
* @property {import('./geo.js').BBox} [bounds]
|
|
121
|
+
* @property {number} [maxzoom]
|
|
122
|
+
* @property {number} [minzoom]
|
|
123
|
+
*/
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
*
|
|
127
|
+
* @param {unknown} tilejson
|
|
128
|
+
* @returns {asserts tilejson is TileJSONPartial}
|
|
129
|
+
*/
|
|
130
|
+
export function assertTileJSON(tilejson) {
|
|
131
|
+
if (typeof tilejson !== 'object' || tilejson === null) {
|
|
132
|
+
throw new Error('Invalid TileJSON')
|
|
133
|
+
}
|
|
134
|
+
if (
|
|
135
|
+
!('tiles' in tilejson) ||
|
|
136
|
+
!Array.isArray(tilejson.tiles) ||
|
|
137
|
+
tilejson.tiles.length === 0 ||
|
|
138
|
+
tilejson.tiles.some((tile) => typeof tile !== 'string')
|
|
139
|
+
) {
|
|
140
|
+
throw new Error('Invalid TileJSON: missing or invalid tiles property')
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const validateStyle =
|
|
145
|
+
/** @type {{ (style: unknown): style is StyleSpecification, errors: ValidationError[] }} */ (
|
|
146
|
+
(style) => {
|
|
147
|
+
validateStyle.errors = validateStyleMin(
|
|
148
|
+
/** @type {StyleSpecification} */ (style),
|
|
149
|
+
)
|
|
150
|
+
if (validateStyle.errors.length) return false
|
|
151
|
+
return true
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Check whether a source is already inlined (e.g. does not reference a TileJSON or GeoJSON url)
|
|
157
|
+
*
|
|
158
|
+
* @param {import('@maplibre/maplibre-gl-style-spec').SourceSpecification} source
|
|
159
|
+
* @returns {source is import('../types.js').InlinedSource}
|
|
160
|
+
*/
|
|
161
|
+
export function isInlinedSource(source) {
|
|
162
|
+
if (source.type === 'geojson') {
|
|
163
|
+
return typeof source.data === 'object'
|
|
164
|
+
} else if (
|
|
165
|
+
source.type === 'vector' ||
|
|
166
|
+
source.type === 'raster' ||
|
|
167
|
+
source.type === 'raster-dem'
|
|
168
|
+
) {
|
|
169
|
+
return 'tiles' in source
|
|
170
|
+
} else {
|
|
171
|
+
// Video and image sources are not strictly "inlined", but we treat them as such.
|
|
172
|
+
return true
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export const URI_SCHEME = 'smp' // "Styled Map Package"
|
|
2
|
+
export const URI_BASE = URI_SCHEME + '://maps.v1/'
|
|
3
|
+
|
|
4
|
+
// These constants determine the file format structure
|
|
5
|
+
export const STYLE_FILE = 'style.json'
|
|
6
|
+
const SOURCES_FOLDER = 's'
|
|
7
|
+
const SPRITES_FOLDER = 'sprites'
|
|
8
|
+
const FONTS_FOLDER = 'fonts'
|
|
9
|
+
|
|
10
|
+
// This must include placeholders `{z}`, `{x}`, `{y}`, since these are used to
|
|
11
|
+
// define the tile URL, and this is a TileJSON standard.
|
|
12
|
+
// The folder here is just `s` to minimize bytes used for filenames, which are
|
|
13
|
+
// included in the header of every tile in the zip file.
|
|
14
|
+
const TILE_FILE = SOURCES_FOLDER + '/{sourceId}/{z}/{x}/{y}{ext}'
|
|
15
|
+
// The pixel ratio and ext placeholders must be at the end of the string with no
|
|
16
|
+
// data between them, because this is the format defined in the MapLibre style spec.
|
|
17
|
+
const SPRITE_FILE = SPRITES_FOLDER + '/{id}/sprite{pixelRatio}{ext}'
|
|
18
|
+
// This must include placeholders `{fontstack}` and `{range}`, since these are
|
|
19
|
+
// part of the MapLibre style spec.
|
|
20
|
+
const GLYPH_FILE = FONTS_FOLDER + '/{fontstack}/{range}.pbf.gz'
|
|
21
|
+
export const GLYPH_URI = URI_BASE + GLYPH_FILE
|
|
22
|
+
|
|
23
|
+
const pathToResouceType = /** @type {const} */ ({
|
|
24
|
+
[TILE_FILE.split('/')[0] + '/']: 'tile',
|
|
25
|
+
[SPRITE_FILE.split('/')[0] + '/']: 'sprite',
|
|
26
|
+
[GLYPH_FILE.split('/')[0] + '/']: 'glyph',
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} path
|
|
31
|
+
* @returns
|
|
32
|
+
*/
|
|
33
|
+
export function getResourceType(path) {
|
|
34
|
+
if (path === 'style.json') return 'style'
|
|
35
|
+
for (const [prefix, type] of Object.entries(pathToResouceType)) {
|
|
36
|
+
if (path.startsWith(prefix)) return type
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`Unknown resource type for path: ${path}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Determine the content type of a file based on its extension.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} path
|
|
45
|
+
*/
|
|
46
|
+
export function getContentType(path) {
|
|
47
|
+
if (path.endsWith('.json')) return 'application/json; charset=utf-8'
|
|
48
|
+
if (path.endsWith('.pbf.gz') || path.endsWith('.pbf'))
|
|
49
|
+
return 'application/x-protobuf'
|
|
50
|
+
if (path.endsWith('.png')) return 'image/png'
|
|
51
|
+
if (path.endsWith('.jpg')) return 'image/jpeg'
|
|
52
|
+
if (path.endsWith('.webp')) return 'image/webp'
|
|
53
|
+
if (path.endsWith('.mvt.gz') || path.endsWith('.mvt'))
|
|
54
|
+
return 'application/vnd.mapbox-vector-tile'
|
|
55
|
+
throw new Error(`Unknown content type for path: ${path}`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get the filename for a tile, given the TileInfo
|
|
60
|
+
*
|
|
61
|
+
* @param {import("type-fest").SetRequired<import("../writer.js").TileInfo, 'format'>} tileInfo
|
|
62
|
+
* @returns
|
|
63
|
+
*/
|
|
64
|
+
export function getTileFilename({ sourceId, z, x, y, format }) {
|
|
65
|
+
const ext = '.' + format + (format === 'mvt' ? '.gz' : '')
|
|
66
|
+
return replaceVariables(TILE_FILE, { sourceId, z, x, y, ext })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get a filename for a sprite file, given the sprite id, pixel ratio and extension
|
|
71
|
+
*
|
|
72
|
+
* @param {{ id: string, pixelRatio: number, ext: '.json' | '.png'}} spriteInfo
|
|
73
|
+
*/
|
|
74
|
+
export function getSpriteFilename({ id, pixelRatio, ext }) {
|
|
75
|
+
return replaceVariables(SPRITE_FILE, {
|
|
76
|
+
id,
|
|
77
|
+
pixelRatio: getPixelRatioString(pixelRatio),
|
|
78
|
+
ext,
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get the filename for a glyph file, given the fontstack and range
|
|
84
|
+
*
|
|
85
|
+
* @param {object} options
|
|
86
|
+
* @param {string} options.fontstack
|
|
87
|
+
* @param {import("../writer.js").GlyphRange} options.range
|
|
88
|
+
*/
|
|
89
|
+
export function getGlyphFilename({ fontstack, range }) {
|
|
90
|
+
return replaceVariables(GLYPH_FILE, { fontstack, range })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get the URI template for the sprites in the style
|
|
95
|
+
*/
|
|
96
|
+
export function getSpriteUri(id = 'default') {
|
|
97
|
+
return (
|
|
98
|
+
URI_BASE + replaceVariables(SPRITE_FILE, { id, pixelRatio: '', ext: '' })
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get the URI template for tiles in the style
|
|
104
|
+
*
|
|
105
|
+
* @param {object} opts
|
|
106
|
+
* @param {string} opts.sourceId
|
|
107
|
+
* @param {import("../writer.js").TileFormat} opts.format
|
|
108
|
+
* @returns
|
|
109
|
+
*/
|
|
110
|
+
export function getTileUri({ sourceId, format }) {
|
|
111
|
+
const ext = '.' + format + (format === 'mvt' ? '.gz' : '')
|
|
112
|
+
return (
|
|
113
|
+
URI_BASE + TILE_FILE.replace('{sourceId}', sourceId).replace('{ext}', ext)
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {number} pixelRatio
|
|
119
|
+
*/
|
|
120
|
+
function getPixelRatioString(pixelRatio) {
|
|
121
|
+
return pixelRatio === 1 ? '' : `@${pixelRatio}x`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Replaces variables in a string with values provided in an object. Variables
|
|
126
|
+
* in the string are denoted by curly braces, e.g., {variableName}.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} template - The string containing variables wrapped in curly braces.
|
|
129
|
+
* @param {Record<string, string | number>} variables - An object where the keys correspond to variable names and values correspond to the replacement values.
|
|
130
|
+
* @returns {string} The string with the variables replaced by their corresponding values.
|
|
131
|
+
*/
|
|
132
|
+
export function replaceVariables(template, variables) {
|
|
133
|
+
return template.replace(/{(.*?)}/g, (match, varName) => {
|
|
134
|
+
return varName in variables ? String(variables[varName]) : match
|
|
135
|
+
})
|
|
136
|
+
}
|