@expofp/offline 0.0.0-experimental.2d6c681

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.
@@ -0,0 +1,343 @@
1
+ import { Box, projectLocalToGps } from '@expofp/geometry';
2
+ import debug from 'debug';
3
+ import { resolveMapTheme } from './map-theme.js';
4
+ import { createOfflineMapStyle, OFFLINE_GLYPH_FONTSTACK } from './offline-map-style.js';
5
+ const log = debug('efp:offline:generate-offline-map-data');
6
+ const MAPS_BASE_PATH = 'maps/';
7
+ const MAP_STYLE_PATH = `${MAPS_BASE_PATH}style.json`;
8
+ const MAP_PMTILES_PATH = `${MAPS_BASE_PATH}basemap.pmtiles`;
9
+ const MAP_PMTILES_STYLE_URL = './basemap.pmtiles';
10
+ // Pannable-bounds padding for the offline tile region — sized off the viewport at
11
+ // max zoom-out (not the canvas span), so a small area map is extended enough to be
12
+ // seen fully when zoomed out, while a large one only gets a rotation margin. The
13
+ // online runtime pans freely (no maxBounds), so this only bounds how far out
14
+ // offline tiles are pre-generated; a safety factor keeps that region comfortably
15
+ // covered across screen sizes and out to the panned edges.
16
+ const MAP_MAX_ZOOM_OUT = 14;
17
+ const MERCATOR_M_PER_PX_Z0 = 78271.5168;
18
+ const M_PER_DEG_LAT = 110540;
19
+ const OFFLINE_REF_VIEWPORT_W = 3840;
20
+ const OFFLINE_REF_VIEWPORT_H = 2160;
21
+ const MAP_BOUNDS_MIN_PADDING_FRACTION = 0.05;
22
+ const OFFLINE_PADDING_SAFETY = 1.5;
23
+ const DEFAULT_MAP_SOURCE_BASE = 'https://build.protomaps.com';
24
+ const DEFAULT_MAP_SOURCE_LOOKBACK_DAYS = 7;
25
+ // Protomaps ships one sprite per palette; the icons are baked light-on-dark or
26
+ // dark-on-light, so the sprite has to follow the theme. (The `white` set this
27
+ // replaced was white-on-white against the light basemap.)
28
+ const PROTOMAPS_SPRITE_BASE = 'https://protomaps.github.io/basemaps-assets/sprites/v4';
29
+ const PROTOMAPS_FONT_BASE = 'https://protomaps.github.io/basemaps-assets/fonts';
30
+ const MAP_GLYPHS_STYLE_URL = './fonts/{fontstack}/{range}.pbf';
31
+ const GLYPH_RANGES = createGlyphRanges(0, 8447);
32
+ const tempDirExitCleanups = new Map();
33
+ export function generateOfflineMapData(options) {
34
+ const maplibre = {
35
+ styleUrl: `./${MAP_STYLE_PATH}`,
36
+ offline: true,
37
+ };
38
+ // The drawing decides the palette and the drawing is frozen inside the
39
+ // archive, so the theme is resolved once here and baked into the style —
40
+ // the runtime has no basis to re-decide it offline.
41
+ const theme = deriveMapTheme(options.fpSvgContext);
42
+ log('Resolved offline basemap theme', theme);
43
+ async function* files() {
44
+ const bbox = derivePaddedVenueBbox(options.fpSvgContext);
45
+ log('Extracting PMTiles for bbox', bbox.join(','));
46
+ const pmtilesFile = await extractPmtiles(options.mapSource, bbox, options.signal);
47
+ try {
48
+ yield {
49
+ url: pmtilesFile.url,
50
+ targetFilePath: MAP_PMTILES_PATH,
51
+ };
52
+ yield {
53
+ text: JSON.stringify(createOfflineMapStyle({
54
+ theme,
55
+ tiles: MAP_PMTILES_STYLE_URL,
56
+ sprite: `./sprites/${spriteName(theme)}`,
57
+ glyphs: MAP_GLYPHS_STYLE_URL,
58
+ }), null, 2),
59
+ targetFilePath: MAP_STYLE_PATH,
60
+ };
61
+ yield* getSpriteAssets(theme);
62
+ yield* getGlyphAssets();
63
+ }
64
+ finally {
65
+ await cleanupTempDir(pmtilesFile.tempDir);
66
+ }
67
+ }
68
+ return { files: files(), maplibre };
69
+ }
70
+ export async function validateOfflineMapOptions(options, signal) {
71
+ let mapSource = options.mapSource;
72
+ if (!mapSource) {
73
+ console.log('Looking for latest Protomaps map source...');
74
+ mapSource = await resolveDefaultMapSource(signal);
75
+ }
76
+ await assertValidMapSource(mapSource);
77
+ console.log(`Using map source ${mapSource}`);
78
+ await assertPmtilesCliAvailable(signal);
79
+ return { ...options, mapSource };
80
+ }
81
+ /** An offline map applies only when the (already-loaded) drawing is geo-referenced. */
82
+ export async function inferOfflineMapOptions(fpSvgContext, options, signal) {
83
+ const fpGeo = fpSvgContext.__fpGeo;
84
+ if (!fpGeo?.properties?.config)
85
+ return undefined;
86
+ return {
87
+ ...(await validateOfflineMapOptions(options ?? {}, signal)),
88
+ fpSvgContext,
89
+ };
90
+ }
91
+ async function resolveDefaultMapSource(signal) {
92
+ for (let dayOffset = 0; dayOffset < DEFAULT_MAP_SOURCE_LOOKBACK_DAYS; dayOffset++) {
93
+ const source = createDefaultMapSource(dayOffset);
94
+ const response = await fetch(source, { method: 'HEAD', signal });
95
+ if (response.ok)
96
+ return source;
97
+ if (response.status !== 404) {
98
+ throw new Error(`Unable to check default offline map source ${source}: HTTP ${response.status}`);
99
+ }
100
+ }
101
+ throw new Error(`Could not find a default Protomaps PMTiles build from the last ${DEFAULT_MAP_SOURCE_LOOKBACK_DAYS} days. Specify --debug-map-source explicitly.`);
102
+ }
103
+ function createDefaultMapSource(dayOffset) {
104
+ const date = new Date();
105
+ date.setUTCDate(date.getUTCDate() - dayOffset);
106
+ const year = date.getUTCFullYear();
107
+ const month = String(date.getUTCMonth() + 1).padStart(2, '0');
108
+ const day = String(date.getUTCDate()).padStart(2, '0');
109
+ return `${DEFAULT_MAP_SOURCE_BASE}/${year}${month}${day}.pmtiles`;
110
+ }
111
+ function derivePaddedVenueBbox(ctx) {
112
+ const fpGeo = ctx.__fpGeo;
113
+ const config = fpGeo?.properties?.config;
114
+ if (!config) {
115
+ throw new Error('Offline map generation requires fpGeo.properties.config.');
116
+ }
117
+ const mapBounds = deriveMapBoundsRect(ctx);
118
+ const [anchorA, anchorB] = geoAnchors(config);
119
+ const corners = [
120
+ [mapBounds.min.x, mapBounds.min.y],
121
+ [mapBounds.max.x, mapBounds.min.y],
122
+ [mapBounds.max.x, mapBounds.max.y],
123
+ [mapBounds.min.x, mapBounds.max.y],
124
+ ].map(([x, y]) => {
125
+ const { lat, lng } = projectLocalToGps({ x, y }, anchorA, anchorB);
126
+ return [lng, lat];
127
+ });
128
+ if (!corners.every(([lng, lat]) => Number.isFinite(lng) && Number.isFinite(lat))) {
129
+ throw new Error('Offline map generation produced invalid map GPS bounds.');
130
+ }
131
+ const lngs = corners.map(([lng]) => lng);
132
+ const lats = corners.map(([, lat]) => lat);
133
+ const minLng = Math.min(...lngs);
134
+ const maxLng = Math.max(...lngs);
135
+ const minLat = Math.min(...lats);
136
+ const maxLat = Math.max(...lats);
137
+ const centerLat = (minLat + maxLat) / 2;
138
+ const cosLat = Math.max(0.01, Math.cos((centerLat * Math.PI) / 180));
139
+ const mPerDegLng = 111320 * cosLat;
140
+ // Work in metres (Mercator is isotropic in metres). Content span = canvas AABB.
141
+ const contentW = (maxLng - minLng) * mPerDegLng;
142
+ const contentH = (maxLat - minLat) * M_PER_DEG_LAT;
143
+ // Viewport span at max zoom-out, for a generous reference screen.
144
+ const mPerPx = (MERCATOR_M_PER_PX_Z0 * cosLat) / 2 ** MAP_MAX_ZOOM_OUT;
145
+ const viewW = OFFLINE_REF_VIEWPORT_W * mPerPx;
146
+ const viewH = OFFLINE_REF_VIEWPORT_H * mPerPx;
147
+ // That viewport rotated by the view bearing has a larger axis-aligned footprint.
148
+ const bearing = config.bearing ?? 0;
149
+ const c = Math.abs(Math.cos((bearing * Math.PI) / 180));
150
+ const s = Math.abs(Math.sin((bearing * Math.PI) / 180));
151
+ const viewRotW = viewW * c + viewH * s;
152
+ const viewRotH = viewW * s + viewH * c;
153
+ // Same rule as online (extend small / constant for large / floor), times safety.
154
+ const padW = Math.max((viewRotW - contentW) / 2, (viewRotW - viewW) / 2, viewW * MAP_BOUNDS_MIN_PADDING_FRACTION) * OFFLINE_PADDING_SAFETY;
155
+ const padH = Math.max((viewRotH - contentH) / 2, (viewRotH - viewH) / 2, viewH * MAP_BOUNDS_MIN_PADDING_FRACTION) * OFFLINE_PADDING_SAFETY;
156
+ const lngPadding = padW / mPerDegLng;
157
+ const latPadding = padH / M_PER_DEG_LAT;
158
+ return [minLng - lngPadding, minLat - latPadding, maxLng + lngPadding, maxLat + latPadding];
159
+ }
160
+ // The offline map extent — the whole SVG canvas, NOT the fit box: the full
161
+ // viewBox unioned with the explicit authored area (`__viewbox` / `rect#VIEWBOX`).
162
+ // Basing this on the
163
+ // authored area alone would, for a small venue `__viewbox`, preload only the
164
+ // venue's tiles and leave the rest of the pannable area blank offline.
165
+ function deriveMapBoundsRect(ctx) {
166
+ const fpSvg = ctx.__fp;
167
+ const viewBox = typeof fpSvg === 'string' ? getSvgViewBox(fpSvg) : null;
168
+ const explicit = deriveExplicitArea(ctx, fpSvg);
169
+ if (viewBox && explicit)
170
+ return Box.fromUnion(viewBox, explicit);
171
+ if (viewBox)
172
+ return viewBox;
173
+ if (explicit)
174
+ return explicit;
175
+ throw new Error('Offline map generation could not derive the map bounds (no SVG viewBox or VIEWBOX rect).');
176
+ }
177
+ // The explicit author-drawn area, if any: the `__viewbox` global or a
178
+ // `<rect id="VIEWBOX">` element. Null when neither is present.
179
+ function deriveExplicitArea(ctx, fpSvg) {
180
+ const viewboxObj = ctx.__viewbox;
181
+ if (viewboxObj) {
182
+ return new Box(viewboxObj.x, viewboxObj.y, viewboxObj.width, viewboxObj.height);
183
+ }
184
+ if (typeof fpSvg === 'string') {
185
+ const viewboxRect = getSvgRectById(fpSvg, 'VIEWBOX');
186
+ if (viewboxRect)
187
+ return viewboxRect;
188
+ }
189
+ return null;
190
+ }
191
+ function getSvgViewBox(svg) {
192
+ const match = /\bviewBox\s*=\s*["']([^"']+)["']/i.exec(svg);
193
+ if (!match)
194
+ return null;
195
+ const [x, y, w, h] = match[1]
196
+ .trim()
197
+ .split(/[\s,]+/)
198
+ .map(Number);
199
+ if (![x, y, w, h].every(Number.isFinite))
200
+ return null;
201
+ return new Box(x, y, w, h);
202
+ }
203
+ function getSvgRectById(svg, id) {
204
+ const rectMatch = new RegExp(`<rect\\b(?=[^>]*\\bid=["']${id}["'])[^>]*/?>`, 'i').exec(svg);
205
+ if (!rectMatch)
206
+ return null;
207
+ const rect = rectMatch[0];
208
+ const x = Number(getSvgAttribute(rect, 'x') ?? 0);
209
+ const y = Number(getSvgAttribute(rect, 'y') ?? 0);
210
+ const w = Number(getSvgAttribute(rect, 'width'));
211
+ const h = Number(getSvgAttribute(rect, 'height'));
212
+ if (![x, y, w, h].every(Number.isFinite))
213
+ return null;
214
+ return new Box(x, y, w, h);
215
+ }
216
+ function getSvgAttribute(element, name) {
217
+ return new RegExp(`\\b${name}\\s*=\\s*["']([^"']+)["']`, 'i').exec(element)?.[1];
218
+ }
219
+ async function assertPmtilesCliAvailable(signal) {
220
+ try {
221
+ await runPmtiles(['version'], signal);
222
+ }
223
+ catch (error) {
224
+ if (signal?.aborted)
225
+ throw error;
226
+ throw new Error(`Offline map extraction requires the pmtiles CLI. Install it with "brew install pmtiles" and try again.`, { cause: error });
227
+ }
228
+ }
229
+ async function assertValidMapSource(mapSource) {
230
+ const source = mapSource.trim();
231
+ if (!source || source !== mapSource || source.startsWith('-')) {
232
+ throw new Error('Offline map --debug-map-source must be an http(s)/file URL or absolute path.');
233
+ }
234
+ const path = await import('node:path');
235
+ if (path.isAbsolute(source) || path.win32.isAbsolute(source))
236
+ return;
237
+ try {
238
+ const url = new URL(source);
239
+ if (url.protocol === 'https:' || url.protocol === 'http:' || url.protocol === 'file:')
240
+ return;
241
+ }
242
+ catch {
243
+ // Fall through to the consistent validation error below.
244
+ }
245
+ throw new Error('Offline map --debug-map-source must be an http(s)/file URL or absolute path.');
246
+ }
247
+ async function extractPmtiles(mapSource, bbox, signal) {
248
+ const { mkdtemp } = await import('node:fs/promises');
249
+ const { tmpdir } = await import('node:os');
250
+ const path = await import('node:path');
251
+ const { pathToFileURL } = await import('node:url');
252
+ throwIfAborted(signal);
253
+ const tempDir = await mkdtemp(path.join(tmpdir(), 'expofp-offline-map-'));
254
+ await registerTempDirExitCleanup(tempDir);
255
+ const targetPath = path.join(tempDir, 'basemap.pmtiles');
256
+ try {
257
+ throwIfAborted(signal);
258
+ await runPmtiles(['extract', mapSource, targetPath, `--bbox=${bbox.join(',')}`], signal);
259
+ return { url: pathToFileURL(targetPath), tempDir };
260
+ }
261
+ catch (error) {
262
+ await cleanupTempDir(tempDir);
263
+ throw error;
264
+ }
265
+ }
266
+ async function cleanupTempDir(tempDir) {
267
+ const { rm } = await import('node:fs/promises');
268
+ tempDirExitCleanups.get(tempDir)?.();
269
+ tempDirExitCleanups.delete(tempDir);
270
+ await rm(tempDir, { recursive: true, force: true });
271
+ }
272
+ async function registerTempDirExitCleanup(tempDir) {
273
+ if (typeof process === 'undefined' || !process.versions?.node)
274
+ return;
275
+ const { rmSync } = await import('node:fs');
276
+ const cleanup = () => rmSync(tempDir, { recursive: true, force: true });
277
+ tempDirExitCleanups.set(tempDir, () => process.removeListener('exit', cleanup));
278
+ process.once('exit', cleanup);
279
+ }
280
+ function throwIfAborted(signal) {
281
+ if (!signal?.aborted)
282
+ return;
283
+ throw signal.reason instanceof Error ? signal.reason : new Error('Operation aborted.');
284
+ }
285
+ async function runPmtiles(args, signal) {
286
+ const { execFile } = await import('node:child_process');
287
+ await new Promise((resolvePromise, reject) => {
288
+ execFile('pmtiles', args, { signal }, (error, stdout, stderr) => {
289
+ if (stdout)
290
+ log(stdout.trim());
291
+ if (stderr)
292
+ log(stderr.trim());
293
+ if (error) {
294
+ // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
295
+ reject(error);
296
+ return;
297
+ }
298
+ resolvePromise();
299
+ });
300
+ });
301
+ }
302
+ function deriveMapTheme(ctx) {
303
+ const fpGeo = ctx.__fpGeo;
304
+ const fpSettings = ctx.__fpSettings;
305
+ return resolveMapTheme(fpGeo?.properties?.config?.style, fpSettings?.backgroundColor);
306
+ }
307
+ function spriteName(theme) {
308
+ return `protomaps-${theme}`;
309
+ }
310
+ function* getSpriteAssets(theme) {
311
+ const source = `${PROTOMAPS_SPRITE_BASE}/${theme}`;
312
+ const targetBase = `${MAPS_BASE_PATH}sprites/${spriteName(theme)}`;
313
+ yield { url: new URL(`${source}.json`), targetFilePath: `${targetBase}.json` };
314
+ yield { url: new URL(`${source}.png`), targetFilePath: `${targetBase}.png` };
315
+ yield { url: new URL(`${source}@2x.json`), targetFilePath: `${targetBase}@2x.json` };
316
+ yield { url: new URL(`${source}@2x.png`), targetFilePath: `${targetBase}@2x.png` };
317
+ }
318
+ function* getGlyphAssets() {
319
+ const encodedFontstack = encodeURIComponent(OFFLINE_GLYPH_FONTSTACK);
320
+ for (const range of GLYPH_RANGES) {
321
+ yield {
322
+ url: new URL(`${PROTOMAPS_FONT_BASE}/${encodedFontstack}/${range}.pbf`),
323
+ targetFilePath: `${MAPS_BASE_PATH}fonts/${OFFLINE_GLYPH_FONTSTACK}/${range}.pbf`,
324
+ };
325
+ }
326
+ }
327
+ function createGlyphRanges(min, max) {
328
+ const ranges = [];
329
+ for (let start = min; start <= max; start += 256) {
330
+ ranges.push(`${start}-${start + 255}`);
331
+ }
332
+ return ranges;
333
+ }
334
+ // Builds the two-anchor affine bridge (p0 -> anchor a, p2 -> anchor b) used to
335
+ // project the SVG-area corners into GPS. All GeoConfig parsing / SVG extraction /
336
+ // anchor validation stays at the callsite; only the pure projection math lives in
337
+ // @expofp/geometry's geo module.
338
+ function geoAnchors(config) {
339
+ return [
340
+ { local: { x: config.p0.x, y: config.p0.y }, geo: { lat: config.p0.lat, lng: config.p0.lng } },
341
+ { local: { x: config.p2.x, y: config.p2.y }, geo: { lat: config.p2.lat, lng: config.p2.lng } },
342
+ ];
343
+ }
@@ -0,0 +1,5 @@
1
+ import { type LocalDataFetch } from './types.js';
2
+ export declare function generateRuntimeFilesData(runtimeBaseUrl: string): AsyncGenerator<LocalDataFetch, {
3
+ entry: string;
4
+ }, void>;
5
+ //# sourceMappingURL=generate-runtime-files-data.d.ts.map
@@ -0,0 +1,16 @@
1
+ import { importJson } from '@expofp/utils';
2
+ import debug from 'debug';
3
+ const log = debug('efp:offline:generate-runtime-files-data');
4
+ export async function* generateRuntimeFilesData(runtimeBaseUrl) {
5
+ const bundleJsonUrl = new URL('bundle.json', runtimeBaseUrl).href;
6
+ const bundleJson = await importJson(bundleJsonUrl);
7
+ log(`Generating runtime files from ${bundleJsonUrl}, ${bundleJson.files.length} files found`);
8
+ const basePath = 'runtime/';
9
+ for (const file of bundleJson.files) {
10
+ yield {
11
+ url: new URL(file.file, runtimeBaseUrl),
12
+ targetFilePath: `${basePath}${file.file}`,
13
+ };
14
+ }
15
+ return { entry: `./${basePath}${bundleJson.entry}` };
16
+ }
@@ -0,0 +1,15 @@
1
+ import { type MapTheme } from '@expofp/schema';
2
+ /**
3
+ * The palette implied by the drawing's background color — the fallback for
4
+ * drawings that predate `__fpGeo.properties.config.style`.
5
+ *
6
+ * `'none'` is a real authored value and is not a parseable color, so it and
7
+ * anything else unparseable resolve to the light palette.
8
+ */
9
+ export declare function deriveMapThemeFromBackground(backgroundColor: string | undefined): MapTheme;
10
+ /**
11
+ * Resolve the basemap palette: the one the drawing declares in its geo config,
12
+ * else the one its background color implies.
13
+ */
14
+ export declare function resolveMapTheme(drawingTheme: MapTheme | undefined, backgroundColor: string | undefined): MapTheme;
15
+ //# sourceMappingURL=map-theme.d.ts.map
@@ -0,0 +1,48 @@
1
+ import Color from 'color';
2
+ /**
3
+ * Which basemap palette a drawing is authored for.
4
+ *
5
+ * A deliberate duplicate of the floorplan's
6
+ * `components/Maplibre/utils/map-theme.ts`. The two must agree: online the SDK
7
+ * resolves the palette at runtime, offline the generator resolves it once and
8
+ * bakes the answer into the archived style, and a drawing must not change
9
+ * appearance just because it was taken offline.
10
+ *
11
+ * The background-color fallback below is on its way out; when it goes, both
12
+ * copies collapse to reading the declared style and this file loses its only
13
+ * non-trivial logic.
14
+ */
15
+ /**
16
+ * Below this HSL lightness (0–100) a background counts as dark. Matches the
17
+ * floorplan's threshold — see the note there on why HSL and not WCAG luminance.
18
+ */
19
+ const DARK_BACKGROUND_LIGHTNESS = 50;
20
+ function isMapTheme(value) {
21
+ return value === 'light' || value === 'dark';
22
+ }
23
+ /**
24
+ * The palette implied by the drawing's background color — the fallback for
25
+ * drawings that predate `__fpGeo.properties.config.style`.
26
+ *
27
+ * `'none'` is a real authored value and is not a parseable color, so it and
28
+ * anything else unparseable resolve to the light palette.
29
+ */
30
+ export function deriveMapThemeFromBackground(backgroundColor) {
31
+ if (!backgroundColor || backgroundColor === 'none')
32
+ return 'light';
33
+ try {
34
+ return Color(backgroundColor).lightness() < DARK_BACKGROUND_LIGHTNESS ? 'dark' : 'light';
35
+ }
36
+ catch {
37
+ return 'light';
38
+ }
39
+ }
40
+ /**
41
+ * Resolve the basemap palette: the one the drawing declares in its geo config,
42
+ * else the one its background color implies.
43
+ */
44
+ export function resolveMapTheme(drawingTheme, backgroundColor) {
45
+ if (isMapTheme(drawingTheme))
46
+ return drawingTheme;
47
+ return deriveMapThemeFromBackground(backgroundColor);
48
+ }
@@ -0,0 +1,51 @@
1
+ import { type MapTheme } from '@expofp/schema';
2
+ import { type Flavor } from '@protomaps/basemaps';
3
+ /** The one font stack every label in {@link createOfflineMapStyle} asks for. */
4
+ export declare const OFFLINE_GLYPH_FONTSTACK = "Noto Sans Regular";
5
+ /**
6
+ * The Protomaps flavor recolored to CARTO Positron.
7
+ *
8
+ * `pois` is deliberately absent: it is optional, and leaving it out drops the
9
+ * colored POI icon layers entirely — the CARTO styles show no POI icons, so
10
+ * emitting them would be the loudest departure from the online look.
11
+ */
12
+ export declare const POSITRON_FLAVOR: Flavor;
13
+ /**
14
+ * The Protomaps flavor recolored to CARTO Dark Matter.
15
+ *
16
+ * Dark Matter tints no landuse at all — its parks, landcover and residential
17
+ * blocks are all painted the background color — so every land field here
18
+ * collapses onto {@link DARK_MATTER_BACKGROUND}. That is the intended look, not
19
+ * an omission.
20
+ */
21
+ export declare const DARK_MATTER_FLAVOR: Flavor;
22
+ export interface OfflineMapStyleOptions {
23
+ /** Basemap palette the drawing is authored for. */
24
+ theme: MapTheme;
25
+ /** Tile source URL, relative to the style document. */
26
+ tiles: string;
27
+ /** Sprite base URL, relative to the style document. */
28
+ sprite: string;
29
+ /** Glyph template URL, relative to the style document. */
30
+ glyphs: string;
31
+ }
32
+ /**
33
+ * Build the offline style document.
34
+ *
35
+ * URLs stay relative to the style so the archive is relocatable; the SDK
36
+ * absolutizes them at load time (see `MapLibreConfigSchema`).
37
+ */
38
+ export declare function createOfflineMapStyle(options: OfflineMapStyleOptions): {
39
+ version: number;
40
+ sources: {
41
+ protomaps: {
42
+ type: string;
43
+ attribution: string;
44
+ url: string;
45
+ };
46
+ };
47
+ sprite: string;
48
+ glyphs: string;
49
+ layers: LayerSpecification[];
50
+ };
51
+ //# sourceMappingURL=offline-map-style.d.ts.map