@tileflow/core 0.1.0-alpha.29 → 0.1.0-alpha.30
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 +8 -1
- package/dist/browser.d.ts +66 -5
- package/dist/browser.js +1102 -39
- package/dist/build.d.ts +7 -7
- package/dist/build.js +485 -163
- package/dist/config-reference.json +1399 -1153
- package/dist/{icon-composition-BquPlPPI.d.ts → icon-composition-BiOTwrvD.d.ts} +12 -3
- package/dist/index.d.ts +105 -11
- package/dist/index.js +770 -168
- package/dist/{manifest-types-BZU4zbmI.d.ts → manifest-types-CTT2eLbp.d.ts} +42 -1
- package/dist/manifest.d.ts +2 -2
- package/dist/native.js +1 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +28 -0
- package/docs/authoring-model.md +30 -3
- package/docs/data-is-separate-from-design.md +2 -1
- package/docs/shared-visual-primitives.md +61 -2
- package/docs/theme-transitions-and-blends.md +165 -0
- package/package.json +1 -1
package/dist/browser.js
CHANGED
|
@@ -2065,6 +2065,7 @@ var maxStaticImagePixels = 1280 * 1280;
|
|
|
2065
2065
|
var maximumManifestBytes = 1024 * 1024;
|
|
2066
2066
|
var maximumSessionGrantBytes = 64 * 1024;
|
|
2067
2067
|
var manifestCache = new globalThis.Map();
|
|
2068
|
+
var tileflowThemeBlendLimits = Object.freeze({ maximumThemes: 8, minimumThemes: 2 });
|
|
2068
2069
|
function getTileflowStyleFontFaces(style) {
|
|
2069
2070
|
const input = style.metadata?.[tileflowStyleFontFacesMetadataKey];
|
|
2070
2071
|
if (input === void 0) return [];
|
|
@@ -2201,6 +2202,876 @@ function getTileflowLocationOrigin() {
|
|
|
2201
2202
|
return window.location.origin;
|
|
2202
2203
|
}
|
|
2203
2204
|
|
|
2205
|
+
// src/theme-blend.ts
|
|
2206
|
+
var TileflowThemeBlendError = class extends Error {
|
|
2207
|
+
name = "TileflowThemeBlendError";
|
|
2208
|
+
};
|
|
2209
|
+
var BlendLeaf = class {
|
|
2210
|
+
constructor(values) {
|
|
2211
|
+
this.values = values;
|
|
2212
|
+
}
|
|
2213
|
+
values;
|
|
2214
|
+
};
|
|
2215
|
+
var imageProperties = /* @__PURE__ */ new Set([
|
|
2216
|
+
"icon-image",
|
|
2217
|
+
"fill-pattern",
|
|
2218
|
+
"line-pattern",
|
|
2219
|
+
"background-pattern",
|
|
2220
|
+
"fill-extrusion-pattern"
|
|
2221
|
+
]);
|
|
2222
|
+
var opacityProperties = {
|
|
2223
|
+
background: "background-opacity",
|
|
2224
|
+
circle: "circle-opacity",
|
|
2225
|
+
fill: "fill-opacity",
|
|
2226
|
+
"fill-extrusion": "fill-extrusion-opacity",
|
|
2227
|
+
heatmap: "heatmap-opacity",
|
|
2228
|
+
line: "line-opacity",
|
|
2229
|
+
raster: "raster-opacity"
|
|
2230
|
+
};
|
|
2231
|
+
var featureOperators = /* @__PURE__ */ new Set([
|
|
2232
|
+
"feature-state",
|
|
2233
|
+
"geometry-type",
|
|
2234
|
+
"get",
|
|
2235
|
+
"has",
|
|
2236
|
+
"id",
|
|
2237
|
+
"in",
|
|
2238
|
+
"properties"
|
|
2239
|
+
]);
|
|
2240
|
+
var maximumBranches = 24;
|
|
2241
|
+
function planTileflowThemeBlend(styles) {
|
|
2242
|
+
if (styles.length < 2)
|
|
2243
|
+
throw new TileflowThemeBlendError("A theme blend needs two or more themes.");
|
|
2244
|
+
const [base] = styles;
|
|
2245
|
+
assertSameStructure(styles, base);
|
|
2246
|
+
const paints = [];
|
|
2247
|
+
const switches = [];
|
|
2248
|
+
const imageGroups = collectImageNames(styles, base);
|
|
2249
|
+
const images = /* @__PURE__ */ new Map();
|
|
2250
|
+
let splitLayers = 0;
|
|
2251
|
+
let copiedLayers = 0;
|
|
2252
|
+
const layers = [];
|
|
2253
|
+
for (const [index, layer] of base.layers.entries()) {
|
|
2254
|
+
const variants = styles.map((style) => style.layers[index]);
|
|
2255
|
+
layers.push(...planLayer(variants, String(layer.type)));
|
|
2256
|
+
}
|
|
2257
|
+
const switchesByLayer = /* @__PURE__ */ new Map();
|
|
2258
|
+
for (const entry of switches) {
|
|
2259
|
+
const list = switchesByLayer.get(entry.layer) ?? [];
|
|
2260
|
+
list.push(entry);
|
|
2261
|
+
switchesByLayer.set(entry.layer, list);
|
|
2262
|
+
}
|
|
2263
|
+
const sky = differingRoot(styles, "sky");
|
|
2264
|
+
const light = differingRoot(styles, "light");
|
|
2265
|
+
return {
|
|
2266
|
+
themes: styles.length,
|
|
2267
|
+
paints,
|
|
2268
|
+
images: [...images.values()],
|
|
2269
|
+
switches,
|
|
2270
|
+
...sky === void 0 ? {} : { sky },
|
|
2271
|
+
...light === void 0 ? {} : { light },
|
|
2272
|
+
summary: { splitLayers, copiedLayers, switchedValues: switches.length },
|
|
2273
|
+
styleAt(position) {
|
|
2274
|
+
const at = clampPosition(position, styles.length);
|
|
2275
|
+
const dominant = Math.round(at);
|
|
2276
|
+
const style = {
|
|
2277
|
+
...base,
|
|
2278
|
+
layers: layers.map(
|
|
2279
|
+
(layer) => fillLayer(layer, at, dominant, switchesByLayer.get(String(layer.id)))
|
|
2280
|
+
)
|
|
2281
|
+
};
|
|
2282
|
+
if (sky !== void 0) style.sky = fillTemplate(sky, at);
|
|
2283
|
+
if (light !== void 0) style.light = fillTemplate(light, at);
|
|
2284
|
+
return style;
|
|
2285
|
+
}
|
|
2286
|
+
};
|
|
2287
|
+
function planLayer(variants, type) {
|
|
2288
|
+
const [layer] = variants;
|
|
2289
|
+
const id = String(layer.id);
|
|
2290
|
+
const paint = { ...layer.paint };
|
|
2291
|
+
const layout = { ...layer.layout };
|
|
2292
|
+
const follows = type === "symbol" ? "dominant" : "position";
|
|
2293
|
+
const ownPaints = [];
|
|
2294
|
+
const ownSwitches = [];
|
|
2295
|
+
const driven = [];
|
|
2296
|
+
const paintOf = (variant) => variant.paint ?? {};
|
|
2297
|
+
for (const group of ["layout", "paint"]) {
|
|
2298
|
+
const target = group === "paint" ? paint : layout;
|
|
2299
|
+
for (const property of propertyNames(variants, group)) {
|
|
2300
|
+
const values = variants.map(
|
|
2301
|
+
(variant) => variant[group]?.[property]
|
|
2302
|
+
);
|
|
2303
|
+
if (values.every((value) => same(value, values[0]))) continue;
|
|
2304
|
+
if (imageProperties.has(property)) {
|
|
2305
|
+
const tuples = imageTuples(values);
|
|
2306
|
+
if (tuples && tuples.every((names) => imageGroups.get(names[0])?.size === 1)) {
|
|
2307
|
+
for (const names of tuples) {
|
|
2308
|
+
if (names.every((name) => name === names[0])) continue;
|
|
2309
|
+
images.set(JSON.stringify(names), {
|
|
2310
|
+
names,
|
|
2311
|
+
follows: property === "icon-image" ? "dominant" : "position"
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
continue;
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
if (group === "layout" || values.includes(void 0) || imageProperties.has(property)) {
|
|
2318
|
+
ownSwitches.push({ group, property, values });
|
|
2319
|
+
continue;
|
|
2320
|
+
}
|
|
2321
|
+
if (values.some(isDataDriven)) {
|
|
2322
|
+
driven.push(property);
|
|
2323
|
+
continue;
|
|
2324
|
+
}
|
|
2325
|
+
const template = tryMixTemplate(values);
|
|
2326
|
+
if (template === void 0) {
|
|
2327
|
+
ownSwitches.push({ group, property, values });
|
|
2328
|
+
continue;
|
|
2329
|
+
}
|
|
2330
|
+
ownPaints.push({ property, template });
|
|
2331
|
+
target[property] = template;
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
const planned = { ...layer, paint, layout };
|
|
2335
|
+
let results = [{ layer: planned, extra: [] }];
|
|
2336
|
+
if (driven.length > 0) {
|
|
2337
|
+
const split = type === "symbol" ? void 0 : splitLayer();
|
|
2338
|
+
const copies = split ?? (type === "symbol" ? void 0 : copyLayer());
|
|
2339
|
+
if (copies) results = copies;
|
|
2340
|
+
else
|
|
2341
|
+
for (const property of driven)
|
|
2342
|
+
ownSwitches.push({
|
|
2343
|
+
group: "paint",
|
|
2344
|
+
property,
|
|
2345
|
+
values: variants.map((variant) => paintOf(variant)[property])
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
for (const result of results) {
|
|
2349
|
+
const resultId = String(result.layer.id);
|
|
2350
|
+
const overridden = new Set(result.extra.map((entry) => entry.property));
|
|
2351
|
+
for (const entry of [
|
|
2352
|
+
...ownPaints.filter((own) => !overridden.has(own.property)),
|
|
2353
|
+
...result.extra
|
|
2354
|
+
])
|
|
2355
|
+
paints.push({ layer: resultId, property: entry.property, template: entry.template, follows });
|
|
2356
|
+
for (const entry of ownSwitches) switches.push({ layer: resultId, ...entry });
|
|
2357
|
+
}
|
|
2358
|
+
return results.map((result) => result.layer);
|
|
2359
|
+
function splitLayer() {
|
|
2360
|
+
if (isLegacyFilter(layer.filter)) return void 0;
|
|
2361
|
+
const branches = splitBranches(
|
|
2362
|
+
Object.fromEntries(
|
|
2363
|
+
driven.map((property) => [
|
|
2364
|
+
property,
|
|
2365
|
+
variants.map((variant) => paintOf(variant)[property])
|
|
2366
|
+
])
|
|
2367
|
+
)
|
|
2368
|
+
);
|
|
2369
|
+
if (!branches) return void 0;
|
|
2370
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2371
|
+
for (const branch of branches) {
|
|
2372
|
+
const key = JSON.stringify(branch.valuesByProperty);
|
|
2373
|
+
let group = groups.get(key);
|
|
2374
|
+
if (!group) {
|
|
2375
|
+
const templates = {};
|
|
2376
|
+
for (const [property, values] of Object.entries(branch.valuesByProperty)) {
|
|
2377
|
+
const template = tryMixTemplate(values);
|
|
2378
|
+
if (template === void 0) return void 0;
|
|
2379
|
+
templates[property] = template;
|
|
2380
|
+
}
|
|
2381
|
+
group = { templates, conditions: [] };
|
|
2382
|
+
groups.set(key, group);
|
|
2383
|
+
}
|
|
2384
|
+
group.conditions.push(
|
|
2385
|
+
branch.conditions.length === 1 ? branch.conditions[0] : ["all", ...branch.conditions]
|
|
2386
|
+
);
|
|
2387
|
+
}
|
|
2388
|
+
splitLayers += groups.size;
|
|
2389
|
+
return [...groups.values()].map((group, number) => {
|
|
2390
|
+
const chosen = group.conditions.length === 1 ? group.conditions[0] : ["any", ...group.conditions];
|
|
2391
|
+
return {
|
|
2392
|
+
layer: {
|
|
2393
|
+
...planned,
|
|
2394
|
+
id: `${id}::branch-${number}`,
|
|
2395
|
+
filter: layer.filter === void 0 ? chosen : ["all", layer.filter, chosen],
|
|
2396
|
+
paint: { ...paint, ...group.templates }
|
|
2397
|
+
},
|
|
2398
|
+
extra: Object.entries(group.templates).filter(([, template]) => containsLeaf(template)).map(([property, template]) => ({ property, template }))
|
|
2399
|
+
};
|
|
2400
|
+
});
|
|
2401
|
+
}
|
|
2402
|
+
function copyLayer() {
|
|
2403
|
+
const opacity = opacityProperties[type];
|
|
2404
|
+
if (!opacity || variants.some((variant) => isDataDriven(paintOf(variant)[opacity])))
|
|
2405
|
+
return void 0;
|
|
2406
|
+
copiedLayers += variants.length;
|
|
2407
|
+
return variants.map((variant, theme) => {
|
|
2408
|
+
const weight = new BlendLeaf(variants.map((_2, other) => other === theme ? 1 : 0));
|
|
2409
|
+
const template = scaleByWeight(paint[opacity] ?? 1, weight);
|
|
2410
|
+
const copyPaint = { ...paint, [opacity]: template };
|
|
2411
|
+
for (const property of driven) copyPaint[property] = paintOf(variant)[property];
|
|
2412
|
+
return {
|
|
2413
|
+
layer: { ...planned, id: `${id}::theme-${theme}`, paint: copyPaint },
|
|
2414
|
+
extra: [{ property: opacity, template }]
|
|
2415
|
+
};
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
function fillTileflowThemeBlendTemplate(template, position) {
|
|
2421
|
+
return fillTemplate(template, position);
|
|
2422
|
+
}
|
|
2423
|
+
function fillLayer(layer, position, dominant, layerSwitches) {
|
|
2424
|
+
const filled = { ...layer };
|
|
2425
|
+
for (const group of ["layout", "paint"]) {
|
|
2426
|
+
const values = layer[group];
|
|
2427
|
+
if (!values) continue;
|
|
2428
|
+
const out = {};
|
|
2429
|
+
for (const [key, value] of Object.entries(values)) out[key] = fillTemplate(value, position);
|
|
2430
|
+
for (const entry of layerSwitches ?? []) {
|
|
2431
|
+
if (entry.group !== group) continue;
|
|
2432
|
+
const value = entry.values[dominant];
|
|
2433
|
+
if (value === void 0) delete out[entry.property];
|
|
2434
|
+
else out[entry.property] = value;
|
|
2435
|
+
}
|
|
2436
|
+
if (Object.keys(out).length > 0) filled[group] = out;
|
|
2437
|
+
else delete filled[group];
|
|
2438
|
+
}
|
|
2439
|
+
return filled;
|
|
2440
|
+
}
|
|
2441
|
+
function fillTemplate(node, position) {
|
|
2442
|
+
if (node instanceof BlendLeaf) return mixLeaf(node.values, position);
|
|
2443
|
+
if (Array.isArray(node)) return node.map((item) => fillTemplate(item, position));
|
|
2444
|
+
if (node && typeof node === "object" && containsLeaf(node)) {
|
|
2445
|
+
return Object.fromEntries(
|
|
2446
|
+
Object.entries(node).map(([key, value]) => [key, fillTemplate(value, position)])
|
|
2447
|
+
);
|
|
2448
|
+
}
|
|
2449
|
+
return node;
|
|
2450
|
+
}
|
|
2451
|
+
function containsLeaf(node) {
|
|
2452
|
+
if (node instanceof BlendLeaf) return true;
|
|
2453
|
+
if (Array.isArray(node)) return node.some(containsLeaf);
|
|
2454
|
+
if (node && typeof node === "object") return Object.values(node).some(containsLeaf);
|
|
2455
|
+
return false;
|
|
2456
|
+
}
|
|
2457
|
+
function mixLeaf(values, position) {
|
|
2458
|
+
const at = clampPosition(position, values.length);
|
|
2459
|
+
const index = Math.min(values.length - 2, Math.floor(at));
|
|
2460
|
+
const t = at - index;
|
|
2461
|
+
const from = values[index];
|
|
2462
|
+
const to = values[index + 1];
|
|
2463
|
+
if (t <= 0 || from === to) return from;
|
|
2464
|
+
if (t >= 1) return to;
|
|
2465
|
+
if (typeof from === "number" && typeof to === "number") return from + (to - from) * t;
|
|
2466
|
+
const a = oklab(String(from));
|
|
2467
|
+
const b2 = oklab(String(to));
|
|
2468
|
+
if (!a || !b2) return t < 0.5 ? from : to;
|
|
2469
|
+
return colourFromOklab(a.map((value, channel) => value + (b2[channel] - value) * t));
|
|
2470
|
+
}
|
|
2471
|
+
function clampPosition(position, themes) {
|
|
2472
|
+
if (!Number.isFinite(position)) return 0;
|
|
2473
|
+
return Math.min(themes - 1, Math.max(0, position));
|
|
2474
|
+
}
|
|
2475
|
+
function assertSameStructure(styles, base) {
|
|
2476
|
+
const ids = (style) => style.layers.map((layer) => layer.id);
|
|
2477
|
+
for (const style of styles) {
|
|
2478
|
+
if (!same(ids(style), ids(base)))
|
|
2479
|
+
throw new TileflowThemeBlendError(
|
|
2480
|
+
"Blended themes must have the same layers in the same order."
|
|
2481
|
+
);
|
|
2482
|
+
if (!same(style.sources, base.sources))
|
|
2483
|
+
throw new TileflowThemeBlendError("Blended themes must use the same sources.");
|
|
2484
|
+
if (!same(style.sprite, base.sprite) || !same(style.glyphs, base.glyphs))
|
|
2485
|
+
throw new TileflowThemeBlendError("Blended themes must use the same sprite and glyphs.");
|
|
2486
|
+
for (const [index, layer] of style.layers.entries()) {
|
|
2487
|
+
const other = base.layers[index];
|
|
2488
|
+
for (const key of ["type", "source", "source-layer", "filter", "minzoom", "maxzoom"])
|
|
2489
|
+
if (!same(layer[key], other[key]))
|
|
2490
|
+
throw new TileflowThemeBlendError(
|
|
2491
|
+
`Blended themes differ in ${key} of layer ${JSON.stringify(layer.id)}.`
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
function propertyNames(variants, group) {
|
|
2497
|
+
const names = /* @__PURE__ */ new Set();
|
|
2498
|
+
for (const variant of variants)
|
|
2499
|
+
for (const name of Object.keys(variant[group] ?? {})) names.add(name);
|
|
2500
|
+
return [...names];
|
|
2501
|
+
}
|
|
2502
|
+
function differingRoot(styles, key) {
|
|
2503
|
+
const values = styles.map((style) => style[key]);
|
|
2504
|
+
if (values.every((value) => same(value, values[0]))) return void 0;
|
|
2505
|
+
return tryMixTemplate(values);
|
|
2506
|
+
}
|
|
2507
|
+
function collectImageNames(styles, base) {
|
|
2508
|
+
const uses = /* @__PURE__ */ new Map();
|
|
2509
|
+
for (const [index] of base.layers.entries()) {
|
|
2510
|
+
for (const group of ["layout", "paint"]) {
|
|
2511
|
+
for (const property of imageProperties) {
|
|
2512
|
+
const values = styles.map(
|
|
2513
|
+
(style) => style.layers[index]?.[group]?.[property]
|
|
2514
|
+
);
|
|
2515
|
+
if (values[0] === void 0) continue;
|
|
2516
|
+
for (const names of imageTuples(values) ?? []) {
|
|
2517
|
+
const set = uses.get(names[0]) ?? /* @__PURE__ */ new Set();
|
|
2518
|
+
set.add(JSON.stringify(names));
|
|
2519
|
+
uses.set(names[0], set);
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
return uses;
|
|
2525
|
+
}
|
|
2526
|
+
function imageTuples(values, out = []) {
|
|
2527
|
+
const [first] = values;
|
|
2528
|
+
if (values.every((value) => typeof value === "string")) {
|
|
2529
|
+
out.push(values);
|
|
2530
|
+
return out;
|
|
2531
|
+
}
|
|
2532
|
+
if (!Array.isArray(first) || typeof first[0] !== "string") {
|
|
2533
|
+
return values.every((value) => same(value, first)) ? out : void 0;
|
|
2534
|
+
}
|
|
2535
|
+
const operator = first[0];
|
|
2536
|
+
if (!values.every(
|
|
2537
|
+
(value) => Array.isArray(value) && value[0] === operator && value.length === first.length
|
|
2538
|
+
)) {
|
|
2539
|
+
return void 0;
|
|
2540
|
+
}
|
|
2541
|
+
const at = (index) => values.map((value) => value[index]);
|
|
2542
|
+
const sameAt = (index) => at(index).every((value) => same(value, first[index]));
|
|
2543
|
+
const outputs = [];
|
|
2544
|
+
if (operator === "literal" || operator === "image") outputs.push(1);
|
|
2545
|
+
else if (operator === "coalesce")
|
|
2546
|
+
for (let index = 1; index < first.length; index += 1) outputs.push(index);
|
|
2547
|
+
else if (operator === "match") {
|
|
2548
|
+
if (!sameAt(1)) return void 0;
|
|
2549
|
+
for (let index = 2; index < first.length - 1; index += 2) {
|
|
2550
|
+
if (!sameAt(index)) return void 0;
|
|
2551
|
+
outputs.push(index + 1);
|
|
2552
|
+
}
|
|
2553
|
+
outputs.push(first.length - 1);
|
|
2554
|
+
} else if (operator === "case") {
|
|
2555
|
+
for (let index = 1; index < first.length - 1; index += 2) {
|
|
2556
|
+
if (!sameAt(index)) return void 0;
|
|
2557
|
+
outputs.push(index + 1);
|
|
2558
|
+
}
|
|
2559
|
+
outputs.push(first.length - 1);
|
|
2560
|
+
} else if (operator === "step") {
|
|
2561
|
+
if (!sameAt(1)) return void 0;
|
|
2562
|
+
outputs.push(2);
|
|
2563
|
+
for (let index = 3; index < first.length; index += 2) {
|
|
2564
|
+
if (!sameAt(index)) return void 0;
|
|
2565
|
+
outputs.push(index + 1);
|
|
2566
|
+
}
|
|
2567
|
+
} else {
|
|
2568
|
+
return values.every((value) => same(value, first)) ? out : void 0;
|
|
2569
|
+
}
|
|
2570
|
+
for (const index of outputs) if (!imageTuples(at(index), out)) return void 0;
|
|
2571
|
+
return out;
|
|
2572
|
+
}
|
|
2573
|
+
function tryMixTemplate(values) {
|
|
2574
|
+
const [first] = values;
|
|
2575
|
+
if (values.every((value) => same(value, first))) return first;
|
|
2576
|
+
if (Array.isArray(first) && first[0] !== "literal" && values.every((value) => Array.isArray(value) && value.length === first.length)) {
|
|
2577
|
+
const items = [];
|
|
2578
|
+
for (let index = 0; index < first.length; index += 1) {
|
|
2579
|
+
const item = tryMixTemplate(values.map((value) => value[index]));
|
|
2580
|
+
if (item === void 0 && first[index] !== void 0) return void 0;
|
|
2581
|
+
items.push(item);
|
|
2582
|
+
}
|
|
2583
|
+
return items;
|
|
2584
|
+
}
|
|
2585
|
+
if (first && typeof first === "object" && !Array.isArray(first)) {
|
|
2586
|
+
const keys = Object.keys(first);
|
|
2587
|
+
if (!values.every((value) => value && typeof value === "object" && same(Object.keys(value), keys)))
|
|
2588
|
+
return void 0;
|
|
2589
|
+
const out = {};
|
|
2590
|
+
for (const key of keys) {
|
|
2591
|
+
const item = tryMixTemplate(values.map((value) => value[key]));
|
|
2592
|
+
if (item === void 0) return void 0;
|
|
2593
|
+
out[key] = item;
|
|
2594
|
+
}
|
|
2595
|
+
return out;
|
|
2596
|
+
}
|
|
2597
|
+
if (values.every((value) => typeof value === "number")) return new BlendLeaf(values);
|
|
2598
|
+
if (values.every((value) => typeof value === "string" && oklab(value)))
|
|
2599
|
+
return new BlendLeaf(values);
|
|
2600
|
+
return void 0;
|
|
2601
|
+
}
|
|
2602
|
+
function isDataDriven(value) {
|
|
2603
|
+
if (!Array.isArray(value)) return false;
|
|
2604
|
+
if (typeof value[0] === "string" && featureOperators.has(value[0])) return true;
|
|
2605
|
+
return value.some(isDataDriven);
|
|
2606
|
+
}
|
|
2607
|
+
var reads = (value, operator) => Array.isArray(value) && (value[0] === operator || value.some((item) => reads(item, operator)));
|
|
2608
|
+
function decisionOf(node) {
|
|
2609
|
+
if (!Array.isArray(node) || typeof node[0] !== "string") return void 0;
|
|
2610
|
+
const [operator] = node;
|
|
2611
|
+
if (operator === "match" && isDataDriven(node[1])) {
|
|
2612
|
+
const labels = [];
|
|
2613
|
+
const outputs = [];
|
|
2614
|
+
for (let index = 2; index < node.length - 1; index += 2) {
|
|
2615
|
+
labels.push(node[index]);
|
|
2616
|
+
outputs.push(node[index + 1]);
|
|
2617
|
+
}
|
|
2618
|
+
outputs.push(node.at(-1));
|
|
2619
|
+
return {
|
|
2620
|
+
index: ["match", node[1], ...labels.flatMap((label, index) => [label, index]), labels.length],
|
|
2621
|
+
outputs
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
if (operator === "case") {
|
|
2625
|
+
const conditions = [];
|
|
2626
|
+
const outputs = [];
|
|
2627
|
+
for (let index = 1; index < node.length - 1; index += 2) {
|
|
2628
|
+
conditions.push(node[index]);
|
|
2629
|
+
outputs.push(node[index + 1]);
|
|
2630
|
+
}
|
|
2631
|
+
outputs.push(node.at(-1));
|
|
2632
|
+
if (!conditions.some(isDataDriven)) return void 0;
|
|
2633
|
+
return {
|
|
2634
|
+
index: [
|
|
2635
|
+
"case",
|
|
2636
|
+
...conditions.flatMap((condition, index) => [condition, index]),
|
|
2637
|
+
conditions.length
|
|
2638
|
+
],
|
|
2639
|
+
outputs
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
if (operator === "step" && isDataDriven(node[1])) {
|
|
2643
|
+
const outputs = [node[2]];
|
|
2644
|
+
const stops = [];
|
|
2645
|
+
for (let index = 3; index < node.length; index += 2) {
|
|
2646
|
+
stops.push(node[index]);
|
|
2647
|
+
outputs.push(node[index + 1]);
|
|
2648
|
+
}
|
|
2649
|
+
return {
|
|
2650
|
+
index: ["step", node[1], 0, ...stops.flatMap((stop, index) => [stop, index + 1])],
|
|
2651
|
+
outputs
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
return void 0;
|
|
2655
|
+
}
|
|
2656
|
+
function firstDecision(value) {
|
|
2657
|
+
const decision = decisionOf(value);
|
|
2658
|
+
if (decision) return decision;
|
|
2659
|
+
if (!Array.isArray(value) || value[0] === "literal") return void 0;
|
|
2660
|
+
for (const item of value) {
|
|
2661
|
+
const found = firstDecision(item);
|
|
2662
|
+
if (found) return found;
|
|
2663
|
+
}
|
|
2664
|
+
return void 0;
|
|
2665
|
+
}
|
|
2666
|
+
function choose(value, key, branch) {
|
|
2667
|
+
const decision = decisionOf(value);
|
|
2668
|
+
if (decision && JSON.stringify(decision.index) === key)
|
|
2669
|
+
return choose(decision.outputs[branch], key, branch);
|
|
2670
|
+
if (!Array.isArray(value) || value[0] === "literal") return value;
|
|
2671
|
+
return value.map((item) => choose(item, key, branch));
|
|
2672
|
+
}
|
|
2673
|
+
function splitBranches(valuesByProperty, conditions = []) {
|
|
2674
|
+
let decision;
|
|
2675
|
+
for (const values of Object.values(valuesByProperty)) {
|
|
2676
|
+
decision = firstDecision(values[0]);
|
|
2677
|
+
if (decision) break;
|
|
2678
|
+
}
|
|
2679
|
+
if (!decision) {
|
|
2680
|
+
const residual = Object.values(valuesByProperty).some((values) => values.some(isDataDriven));
|
|
2681
|
+
return residual ? void 0 : [{ conditions, valuesByProperty }];
|
|
2682
|
+
}
|
|
2683
|
+
if (reads(decision.index, "zoom") || reads(decision.index, "feature-state")) return void 0;
|
|
2684
|
+
const key = JSON.stringify(decision.index);
|
|
2685
|
+
const branches = [];
|
|
2686
|
+
for (let branch = 0; branch < decision.outputs.length; branch += 1) {
|
|
2687
|
+
const chosen = Object.fromEntries(
|
|
2688
|
+
Object.entries(valuesByProperty).map(([property, values]) => [
|
|
2689
|
+
property,
|
|
2690
|
+
values.map((value) => choose(value, key, branch))
|
|
2691
|
+
])
|
|
2692
|
+
);
|
|
2693
|
+
const nested = splitBranches(chosen, [...conditions, ["==", decision.index, branch]]);
|
|
2694
|
+
if (!nested) return void 0;
|
|
2695
|
+
branches.push(...nested);
|
|
2696
|
+
if (branches.length > maximumBranches) return void 0;
|
|
2697
|
+
}
|
|
2698
|
+
return branches;
|
|
2699
|
+
}
|
|
2700
|
+
var isLegacyFilter = (filter) => Array.isArray(filter) && typeof filter[1] === "string" && !Array.isArray(filter[1]);
|
|
2701
|
+
function scaleByWeight(opacity, weight) {
|
|
2702
|
+
const input = Array.isArray(opacity) ? opacity[0] === "interpolate" ? opacity[2] : opacity[1] : void 0;
|
|
2703
|
+
if (Array.isArray(opacity) && (opacity[0] === "interpolate" || opacity[0] === "step") && reads(input, "zoom")) {
|
|
2704
|
+
const head = opacity[0] === "interpolate" ? 3 : 2;
|
|
2705
|
+
return opacity.map(
|
|
2706
|
+
(item, index) => index >= head && (index - head) % 2 === (opacity[0] === "interpolate" ? 1 : 0) ? scaleByWeight(item, weight) : item
|
|
2707
|
+
);
|
|
2708
|
+
}
|
|
2709
|
+
if (typeof opacity === "number")
|
|
2710
|
+
return new BlendLeaf(weight.values.map((value) => opacity * Number(value)));
|
|
2711
|
+
return ["*", opacity, weight];
|
|
2712
|
+
}
|
|
2713
|
+
function same(a, b2) {
|
|
2714
|
+
return JSON.stringify(a) === JSON.stringify(b2);
|
|
2715
|
+
}
|
|
2716
|
+
var oklabCache = /* @__PURE__ */ new Map();
|
|
2717
|
+
function oklab(colour) {
|
|
2718
|
+
const cached = oklabCache.get(colour);
|
|
2719
|
+
if (cached !== void 0) return cached;
|
|
2720
|
+
const rgba = parseColour(colour);
|
|
2721
|
+
let result = null;
|
|
2722
|
+
if (rgba) {
|
|
2723
|
+
const [r, g2, b2] = rgba.slice(0, 3).map((channel) => {
|
|
2724
|
+
const value = channel / 255;
|
|
2725
|
+
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
2726
|
+
});
|
|
2727
|
+
const l2 = Math.cbrt(0.4122214708 * r + 0.5363325363 * g2 + 0.0514459929 * b2);
|
|
2728
|
+
const m2 = Math.cbrt(0.2119034982 * r + 0.6806995451 * g2 + 0.1073969566 * b2);
|
|
2729
|
+
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g2 + 0.6299787005 * b2);
|
|
2730
|
+
result = [
|
|
2731
|
+
0.2104542553 * l2 + 0.793617785 * m2 - 0.0040720468 * s,
|
|
2732
|
+
1.9779984951 * l2 - 2.428592205 * m2 + 0.4505937099 * s,
|
|
2733
|
+
0.0259040371 * l2 + 0.7827717662 * m2 - 0.808675766 * s,
|
|
2734
|
+
rgba[3]
|
|
2735
|
+
];
|
|
2736
|
+
}
|
|
2737
|
+
if (oklabCache.size > 4096) oklabCache.clear();
|
|
2738
|
+
oklabCache.set(colour, result);
|
|
2739
|
+
return result;
|
|
2740
|
+
}
|
|
2741
|
+
function colourFromOklab([lightness, a, b2, alpha]) {
|
|
2742
|
+
const l2 = (lightness + 0.3963377774 * a + 0.2158037573 * b2) ** 3;
|
|
2743
|
+
const m2 = (lightness - 0.1055613458 * a - 0.0638541728 * b2) ** 3;
|
|
2744
|
+
const s = (lightness - 0.0894841775 * a - 1.291485548 * b2) ** 3;
|
|
2745
|
+
const [red, green, blue] = [
|
|
2746
|
+
4.0767416621 * l2 - 3.3077115913 * m2 + 0.2309699292 * s,
|
|
2747
|
+
-1.2684380046 * l2 + 2.6097574011 * m2 - 0.3413193965 * s,
|
|
2748
|
+
-0.0041960863 * l2 - 0.7034186147 * m2 + 1.707614701 * s
|
|
2749
|
+
].map((value) => {
|
|
2750
|
+
const clamped = Math.min(1, Math.max(0, value));
|
|
2751
|
+
const encoded = clamped <= 31308e-7 ? clamped * 12.92 : 1.055 * clamped ** (1 / 2.4) - 0.055;
|
|
2752
|
+
return Math.round(encoded * 255);
|
|
2753
|
+
});
|
|
2754
|
+
return `rgba(${red}, ${green}, ${blue}, ${Number(alpha.toFixed(3))})`;
|
|
2755
|
+
}
|
|
2756
|
+
function parseColour(colour) {
|
|
2757
|
+
const text = colour.trim().toLowerCase();
|
|
2758
|
+
const hex = /^#([\da-f]{3,8})$/u.exec(text)?.[1];
|
|
2759
|
+
if (hex && [3, 4, 6, 8].includes(hex.length)) {
|
|
2760
|
+
const full = hex.length <= 4 ? [...hex].map((digit) => digit + digit).join("") : hex;
|
|
2761
|
+
const channels = full.match(/../gu).map((pair) => Number.parseInt(pair, 16));
|
|
2762
|
+
return [channels[0], channels[1], channels[2], channels.length > 3 ? channels[3] / 255 : 1];
|
|
2763
|
+
}
|
|
2764
|
+
const functional = /^(rgba?|hsla?)\(([^)]+)\)$/u.exec(text);
|
|
2765
|
+
if (!functional) return null;
|
|
2766
|
+
const parts = functional[2].split(/[\s,/]+/u).filter(Boolean);
|
|
2767
|
+
if (parts.length < 3 || parts.length > 4) return null;
|
|
2768
|
+
const alpha = parts[3] === void 0 ? 1 : parseUnit(parts[3], 1);
|
|
2769
|
+
if (functional[1].startsWith("rgb")) {
|
|
2770
|
+
const [r, g2, b2] = parts.slice(0, 3).map((part) => parseUnit(part, 255));
|
|
2771
|
+
if ([r, g2, b2, alpha].some((value) => value === null)) return null;
|
|
2772
|
+
return [r, g2, b2, alpha];
|
|
2773
|
+
}
|
|
2774
|
+
const hue = Number.parseFloat(parts[0]);
|
|
2775
|
+
const saturation = parseUnit(parts[1], 1);
|
|
2776
|
+
const light = parseUnit(parts[2], 1);
|
|
2777
|
+
if (!Number.isFinite(hue) || saturation === null || light === null || alpha === null) return null;
|
|
2778
|
+
const chroma = (1 - Math.abs(2 * light - 1)) * saturation;
|
|
2779
|
+
const sector = (hue % 360 + 360) % 360 / 60;
|
|
2780
|
+
const x3 = chroma * (1 - Math.abs(sector % 2 - 1));
|
|
2781
|
+
const [r1, g1, b1] = sector < 1 ? [chroma, x3, 0] : sector < 2 ? [x3, chroma, 0] : sector < 3 ? [0, chroma, x3] : sector < 4 ? [0, x3, chroma] : sector < 5 ? [x3, 0, chroma] : [chroma, 0, x3];
|
|
2782
|
+
const offset = light - chroma / 2;
|
|
2783
|
+
return [(r1 + offset) * 255, (g1 + offset) * 255, (b1 + offset) * 255, alpha];
|
|
2784
|
+
}
|
|
2785
|
+
function parseUnit(part, scale) {
|
|
2786
|
+
const percentage = part.endsWith("%");
|
|
2787
|
+
const value = Number.parseFloat(part);
|
|
2788
|
+
if (!Number.isFinite(value)) return null;
|
|
2789
|
+
return percentage ? value / 100 * scale : value;
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
// src/theme-motion-browser.ts
|
|
2793
|
+
var settleFrames = 3;
|
|
2794
|
+
var settleTimeoutMs = 1e4;
|
|
2795
|
+
function beginTileflowStyleCrossfade(input, duration) {
|
|
2796
|
+
if (duration <= 0 || !isCrossfadeMap(input)) return void 0;
|
|
2797
|
+
const map = input;
|
|
2798
|
+
const canvas = map.getCanvas();
|
|
2799
|
+
const document2 = canvas.ownerDocument;
|
|
2800
|
+
const view = document2?.defaultView;
|
|
2801
|
+
if (!view || prefersReducedMotion(view)) return void 0;
|
|
2802
|
+
let snapshot;
|
|
2803
|
+
try {
|
|
2804
|
+
map.redraw();
|
|
2805
|
+
snapshot = document2.createElement("canvas");
|
|
2806
|
+
snapshot.width = canvas.width;
|
|
2807
|
+
snapshot.height = canvas.height;
|
|
2808
|
+
const context = snapshot.getContext("2d");
|
|
2809
|
+
if (!context) return void 0;
|
|
2810
|
+
context.drawImage(canvas, 0, 0);
|
|
2811
|
+
} catch {
|
|
2812
|
+
return void 0;
|
|
2813
|
+
}
|
|
2814
|
+
const width = canvas.clientWidth;
|
|
2815
|
+
const height = canvas.clientHeight;
|
|
2816
|
+
Object.assign(snapshot.style, {
|
|
2817
|
+
height: `${height}px`,
|
|
2818
|
+
left: "0",
|
|
2819
|
+
pointerEvents: "none",
|
|
2820
|
+
position: "absolute",
|
|
2821
|
+
top: "0",
|
|
2822
|
+
transformOrigin: "0 0",
|
|
2823
|
+
width: `${width}px`
|
|
2824
|
+
});
|
|
2825
|
+
snapshot.setAttribute("aria-hidden", "true");
|
|
2826
|
+
snapshot.dataset.tileflowThemeCrossfade = "";
|
|
2827
|
+
canvas.after(snapshot);
|
|
2828
|
+
const corners = [
|
|
2829
|
+
[width * 0.1, height * 0.45],
|
|
2830
|
+
[width * 0.9, height * 0.45],
|
|
2831
|
+
[width * 0.9, height * 0.9],
|
|
2832
|
+
[width * 0.1, height * 0.9]
|
|
2833
|
+
];
|
|
2834
|
+
const anchors = corners.map((corner) => map.unproject(corner));
|
|
2835
|
+
const follow = () => {
|
|
2836
|
+
const to = anchors.map((anchor) => map.project(anchor));
|
|
2837
|
+
if (to.some((point) => !Number.isFinite(point.x) || !Number.isFinite(point.y))) return;
|
|
2838
|
+
const matrix = homography(
|
|
2839
|
+
corners,
|
|
2840
|
+
to.map((point) => [point.x, point.y])
|
|
2841
|
+
);
|
|
2842
|
+
if (matrix) snapshot.style.transform = matrix3d(matrix);
|
|
2843
|
+
};
|
|
2844
|
+
map.on("move", follow);
|
|
2845
|
+
let removed = false;
|
|
2846
|
+
const remove = () => {
|
|
2847
|
+
if (removed) return;
|
|
2848
|
+
removed = true;
|
|
2849
|
+
map.off("move", follow);
|
|
2850
|
+
snapshot.remove();
|
|
2851
|
+
};
|
|
2852
|
+
const frame = (callback) => {
|
|
2853
|
+
if (typeof view.requestAnimationFrame === "function") view.requestAnimationFrame(callback);
|
|
2854
|
+
else view.setTimeout(callback, 16);
|
|
2855
|
+
};
|
|
2856
|
+
return {
|
|
2857
|
+
cancel: remove,
|
|
2858
|
+
finish(options = {}) {
|
|
2859
|
+
return new Promise((resolve) => {
|
|
2860
|
+
const started = view.performance?.now?.() ?? Date.now();
|
|
2861
|
+
let settled = 0;
|
|
2862
|
+
const check = () => {
|
|
2863
|
+
if (removed) return resolve();
|
|
2864
|
+
if (options.stop?.()) {
|
|
2865
|
+
remove();
|
|
2866
|
+
return resolve();
|
|
2867
|
+
}
|
|
2868
|
+
const now = view.performance?.now?.() ?? Date.now();
|
|
2869
|
+
settled = map.isStyleLoaded() && map.areTilesLoaded() ? settled + 1 : 0;
|
|
2870
|
+
if (settled < settleFrames && now - started < settleTimeoutMs) {
|
|
2871
|
+
frame(check);
|
|
2872
|
+
return;
|
|
2873
|
+
}
|
|
2874
|
+
snapshot.style.transition = `opacity ${duration}ms ease`;
|
|
2875
|
+
snapshot.style.opacity = "0";
|
|
2876
|
+
view.setTimeout(() => {
|
|
2877
|
+
remove();
|
|
2878
|
+
resolve();
|
|
2879
|
+
}, duration + 20);
|
|
2880
|
+
};
|
|
2881
|
+
frame(() => frame(check));
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2884
|
+
};
|
|
2885
|
+
}
|
|
2886
|
+
function canBlendTileflowThemes(map) {
|
|
2887
|
+
return typeof map.setPaintProperty === "function" && typeof map.setLayoutProperty === "function" && typeof map.getImage === "function" && typeof map.updateImage === "function";
|
|
2888
|
+
}
|
|
2889
|
+
function createTileflowThemeBlender(input, plan, options) {
|
|
2890
|
+
if (!canBlendTileflowThemes(input)) {
|
|
2891
|
+
throw new TypeError("Tileflow theme blends require a MapLibre GL JS map.");
|
|
2892
|
+
}
|
|
2893
|
+
const map = input;
|
|
2894
|
+
const clamp = (value) => Math.min(plan.themes - 1, Math.max(0, Number.isFinite(value) ? value : 0));
|
|
2895
|
+
const view = viewOf(input);
|
|
2896
|
+
const positionPaints = plan.paints.filter((paint) => paint.follows === "position");
|
|
2897
|
+
const dominantPaints = plan.paints.filter((paint) => paint.follows === "dominant");
|
|
2898
|
+
const pixels = createImageMixer(map, plan.images);
|
|
2899
|
+
const shownValues = /* @__PURE__ */ new Map();
|
|
2900
|
+
let position = clamp(options.position);
|
|
2901
|
+
let dominant = Math.round(position);
|
|
2902
|
+
let label = dominant;
|
|
2903
|
+
let fade;
|
|
2904
|
+
let frameRequest;
|
|
2905
|
+
let disposed = false;
|
|
2906
|
+
pixels.paint("position", position);
|
|
2907
|
+
pixels.paint("dominant", label);
|
|
2908
|
+
const drive = (paints, at) => {
|
|
2909
|
+
for (const paint of paints) {
|
|
2910
|
+
const value = fillTileflowThemeBlendTemplate(paint.template, at);
|
|
2911
|
+
const key = JSON.stringify(value);
|
|
2912
|
+
if (shownValues.get(paint) === key) continue;
|
|
2913
|
+
shownValues.set(paint, key);
|
|
2914
|
+
map.setPaintProperty(paint.layer, paint.property, value, { validate: false });
|
|
2915
|
+
}
|
|
2916
|
+
};
|
|
2917
|
+
const now = () => view?.performance?.now?.() ?? Date.now();
|
|
2918
|
+
const step = () => {
|
|
2919
|
+
frameRequest = void 0;
|
|
2920
|
+
if (disposed || !fade) return;
|
|
2921
|
+
const t = options.labelFadeMs <= 0 || !view ? 1 : Math.min(1, (now() - fade.start) / options.labelFadeMs);
|
|
2922
|
+
label = fade.from + (fade.to - fade.from) * t * t * (3 - 2 * t);
|
|
2923
|
+
drive(dominantPaints, label);
|
|
2924
|
+
pixels.paint("dominant", label);
|
|
2925
|
+
if (t >= 1) fade = void 0;
|
|
2926
|
+
else schedule();
|
|
2927
|
+
};
|
|
2928
|
+
const schedule = () => {
|
|
2929
|
+
if (frameRequest !== void 0 || !view) return;
|
|
2930
|
+
frameRequest = typeof view.requestAnimationFrame === "function" ? view.requestAnimationFrame(step) : view.setTimeout(step, 16);
|
|
2931
|
+
};
|
|
2932
|
+
const applySwitches = (theme) => {
|
|
2933
|
+
for (const entry of plan.switches) {
|
|
2934
|
+
const value = entry.values[theme];
|
|
2935
|
+
if (entry.group === "paint")
|
|
2936
|
+
map.setPaintProperty(entry.layer, entry.property, value, { validate: false });
|
|
2937
|
+
else map.setLayoutProperty(entry.layer, entry.property, value, { validate: false });
|
|
2938
|
+
}
|
|
2939
|
+
};
|
|
2940
|
+
return {
|
|
2941
|
+
get dominant() {
|
|
2942
|
+
return dominant;
|
|
2943
|
+
},
|
|
2944
|
+
get position() {
|
|
2945
|
+
return position;
|
|
2946
|
+
},
|
|
2947
|
+
dispose() {
|
|
2948
|
+
disposed = true;
|
|
2949
|
+
if (frameRequest !== void 0 && view) {
|
|
2950
|
+
if (typeof view.cancelAnimationFrame === "function")
|
|
2951
|
+
view.cancelAnimationFrame(frameRequest);
|
|
2952
|
+
view.clearTimeout(frameRequest);
|
|
2953
|
+
}
|
|
2954
|
+
frameRequest = void 0;
|
|
2955
|
+
},
|
|
2956
|
+
set(next) {
|
|
2957
|
+
if (disposed) return;
|
|
2958
|
+
const target = clamp(next);
|
|
2959
|
+
const reachesTheme = target % 1 === 0 && target !== position;
|
|
2960
|
+
if (Math.abs(target - position) >= 25e-4 || reachesTheme) {
|
|
2961
|
+
position = target;
|
|
2962
|
+
drive(positionPaints, position);
|
|
2963
|
+
pixels.paint("position", position);
|
|
2964
|
+
if (plan.sky !== void 0 && typeof map.setSky === "function")
|
|
2965
|
+
map.setSky(fillTileflowThemeBlendTemplate(plan.sky, position), { validate: false });
|
|
2966
|
+
if (plan.light !== void 0 && typeof map.setLight === "function")
|
|
2967
|
+
map.setLight(fillTileflowThemeBlendTemplate(plan.light, position), { validate: false });
|
|
2968
|
+
}
|
|
2969
|
+
const nextDominant = Math.round(target);
|
|
2970
|
+
if (nextDominant === dominant) return;
|
|
2971
|
+
dominant = nextDominant;
|
|
2972
|
+
if (plan.switches.length > 0) {
|
|
2973
|
+
const change = () => applySwitches(nextDominant);
|
|
2974
|
+
if (options.cover) options.cover(change);
|
|
2975
|
+
else change();
|
|
2976
|
+
}
|
|
2977
|
+
fade = { from: label, start: now(), to: nextDominant };
|
|
2978
|
+
if (options.labelFadeMs <= 0 || !view) step();
|
|
2979
|
+
else schedule();
|
|
2980
|
+
}
|
|
2981
|
+
};
|
|
2982
|
+
}
|
|
2983
|
+
function createImageMixer(map, images) {
|
|
2984
|
+
const art = /* @__PURE__ */ new Map();
|
|
2985
|
+
const shown = /* @__PURE__ */ new Map();
|
|
2986
|
+
const unusable = /* @__PURE__ */ new Set();
|
|
2987
|
+
const artOf = (name) => {
|
|
2988
|
+
if (!art.has(name)) {
|
|
2989
|
+
const image = map.getImage(name)?.data;
|
|
2990
|
+
if (!image) return void 0;
|
|
2991
|
+
art.set(name, { data: Uint8Array.from(image.data), height: image.height, width: image.width });
|
|
2992
|
+
}
|
|
2993
|
+
return art.get(name);
|
|
2994
|
+
};
|
|
2995
|
+
return {
|
|
2996
|
+
paint(follows, position) {
|
|
2997
|
+
const quantized = Math.round(position * 32) / 32;
|
|
2998
|
+
for (const image of images) {
|
|
2999
|
+
if (image.follows !== follows || unusable.has(image) || shown.get(image) === quantized)
|
|
3000
|
+
continue;
|
|
3001
|
+
const sources = image.names.map(artOf);
|
|
3002
|
+
if (sources.some((source) => !source)) continue;
|
|
3003
|
+
const [first] = sources;
|
|
3004
|
+
if (sources.some((source) => source.width !== first.width || source.height !== first.height)) {
|
|
3005
|
+
unusable.add(image);
|
|
3006
|
+
continue;
|
|
3007
|
+
}
|
|
3008
|
+
shown.set(image, quantized);
|
|
3009
|
+
const index = Math.min(sources.length - 2, Math.floor(quantized));
|
|
3010
|
+
const t = quantized - index;
|
|
3011
|
+
const from = sources[index];
|
|
3012
|
+
const to = sources[index + 1];
|
|
3013
|
+
let data = from.data;
|
|
3014
|
+
if (t >= 1) data = to.data;
|
|
3015
|
+
else if (t > 0 && from !== to) {
|
|
3016
|
+
data = new Uint8Array(from.data.length);
|
|
3017
|
+
for (let byte = 0; byte < data.length; byte += 1)
|
|
3018
|
+
data[byte] = from.data[byte] + (to.data[byte] - from.data[byte]) * t;
|
|
3019
|
+
}
|
|
3020
|
+
map.updateImage(image.names[0], { data, height: from.height, width: from.width });
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
};
|
|
3024
|
+
}
|
|
3025
|
+
function isCrossfadeMap(map) {
|
|
3026
|
+
return typeof map.getCanvas === "function" && typeof map.redraw === "function" && typeof map.project === "function" && typeof map.unproject === "function" && typeof map.isStyleLoaded === "function" && typeof map.areTilesLoaded === "function";
|
|
3027
|
+
}
|
|
3028
|
+
function viewOf(map) {
|
|
3029
|
+
if (typeof map.getCanvas !== "function") return void 0;
|
|
3030
|
+
try {
|
|
3031
|
+
return map.getCanvas().ownerDocument?.defaultView ?? void 0;
|
|
3032
|
+
} catch {
|
|
3033
|
+
return void 0;
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
function prefersReducedMotion(view) {
|
|
3037
|
+
try {
|
|
3038
|
+
return view.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
|
|
3039
|
+
} catch {
|
|
3040
|
+
return false;
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
function tileflowThemeMotionView(map) {
|
|
3044
|
+
return viewOf(map);
|
|
3045
|
+
}
|
|
3046
|
+
function homography(from, to) {
|
|
3047
|
+
const rows = [];
|
|
3048
|
+
for (let index = 0; index < 4; index += 1) {
|
|
3049
|
+
const [x3, y] = from[index];
|
|
3050
|
+
const [u, v2] = to[index];
|
|
3051
|
+
rows.push([x3, y, 1, 0, 0, 0, -u * x3, -u * y, u]);
|
|
3052
|
+
rows.push([0, 0, 0, x3, y, 1, -v2 * x3, -v2 * y, v2]);
|
|
3053
|
+
}
|
|
3054
|
+
for (let column = 0; column < 8; column += 1) {
|
|
3055
|
+
let pivot = column;
|
|
3056
|
+
for (let row = column + 1; row < 8; row += 1)
|
|
3057
|
+
if (Math.abs(rows[row][column]) > Math.abs(rows[pivot][column])) pivot = row;
|
|
3058
|
+
if (Math.abs(rows[pivot][column]) < 1e-12) return void 0;
|
|
3059
|
+
[rows[column], rows[pivot]] = [rows[pivot], rows[column]];
|
|
3060
|
+
for (let row = 0; row < 8; row += 1) {
|
|
3061
|
+
if (row === column) continue;
|
|
3062
|
+
const factor = rows[row][column] / rows[column][column];
|
|
3063
|
+
for (let k2 = column; k2 <= 8; k2 += 1) rows[row][k2] -= factor * rows[column][k2];
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
return [...rows.map((row, index) => row[8] / row[index]), 1];
|
|
3067
|
+
}
|
|
3068
|
+
function matrix3d(matrix) {
|
|
3069
|
+
const [a, b2, c, d, e, f, g2, h, i2] = matrix.map(
|
|
3070
|
+
(value) => Math.abs(value) < 1e-12 ? 0 : Number(value.toPrecision(10))
|
|
3071
|
+
);
|
|
3072
|
+
return `matrix3d(${a},${d},0,${g2},${b2},${e},0,${h},0,0,1,0,${c},${f},0,${i2})`;
|
|
3073
|
+
}
|
|
3074
|
+
|
|
2204
3075
|
// src/atmosphere.ts
|
|
2205
3076
|
var tileflowAtmosphereMetadataKey = "tileflow:atmosphere";
|
|
2206
3077
|
function readTileflowAtmosphere(value) {
|
|
@@ -4081,11 +4952,22 @@ function handleSystemColorSchemeChange() {
|
|
|
4081
4952
|
systemColorScheme = next;
|
|
4082
4953
|
for (const listener of systemColorSchemeListeners) listener(next);
|
|
4083
4954
|
}
|
|
4955
|
+
var maximumBlendThemes = 8;
|
|
4956
|
+
var maximumTransitionMs = 5e3;
|
|
4957
|
+
var blendLabelFadeMs = 450;
|
|
4958
|
+
var maximumTileflowBlendStyleBytes = 16 * 1024 * 1024;
|
|
4084
4959
|
function createTileflowThemeController(options) {
|
|
4085
4960
|
assertConcreteRuntimeTheme(options.initial, "initial");
|
|
4086
4961
|
const loadFonts = options.loadFonts ?? ((runtimeStyle) => loadTileflowStyleFonts(runtimeStyle.style, { fontFaces: runtimeStyle.fontFaces }));
|
|
4962
|
+
const loadStyle = options.loadStyle ?? loadTileflowBlendStyle;
|
|
4087
4963
|
const timeoutMs = normalizeThemeTransitionTimeout(options.timeoutMs);
|
|
4964
|
+
const defaultTransition = normalizeThemeTransitionDuration(options.transition);
|
|
4965
|
+
const styleIdentities = /* @__PURE__ */ new WeakMap();
|
|
4966
|
+
let nextStyleIdentity = 0;
|
|
4088
4967
|
let current = options.initial;
|
|
4968
|
+
let shown = { style: options.initial.style };
|
|
4969
|
+
let activeBlend;
|
|
4970
|
+
let pendingBlend;
|
|
4089
4971
|
let disposed = false;
|
|
4090
4972
|
let requestId = 0;
|
|
4091
4973
|
let applyQueue = Promise.resolve();
|
|
@@ -4093,23 +4975,67 @@ function createTileflowThemeController(options) {
|
|
|
4093
4975
|
dispose() {
|
|
4094
4976
|
disposed = true;
|
|
4095
4977
|
requestId += 1;
|
|
4978
|
+
activeBlend?.blender.dispose();
|
|
4979
|
+
},
|
|
4980
|
+
getBlend() {
|
|
4981
|
+
if (!activeBlend) return void 0;
|
|
4982
|
+
return {
|
|
4983
|
+
position: activeBlend.blender.position,
|
|
4984
|
+
themes: activeBlend.themes.map((theme) => theme.theme)
|
|
4985
|
+
};
|
|
4096
4986
|
},
|
|
4097
4987
|
getCurrent() {
|
|
4098
4988
|
return current;
|
|
4099
4989
|
},
|
|
4100
|
-
|
|
4990
|
+
setBlend(request, changeOptions) {
|
|
4991
|
+
const targetTheme = nearestTheme(request);
|
|
4992
|
+
const invalid = validateBlendRequest(request);
|
|
4993
|
+
if (invalid) return Promise.resolve({ error: invalid, status: "failed", theme: targetTheme });
|
|
4994
|
+
if (disposed) return Promise.resolve(disposedResult(targetTheme));
|
|
4995
|
+
const transition = transitionOf(changeOptions);
|
|
4996
|
+
if (transition instanceof TypeError)
|
|
4997
|
+
return Promise.resolve({ error: transition, status: "failed", theme: targetTheme });
|
|
4998
|
+
const key = blendKey(request.themes);
|
|
4999
|
+
if (activeBlend?.key === key) {
|
|
5000
|
+
requestId += 1;
|
|
5001
|
+
activeBlend.blender.set(request.position);
|
|
5002
|
+
current = activeBlend.themes[activeBlend.blender.dominant];
|
|
5003
|
+
return Promise.resolve({ status: "applied", theme: current.theme });
|
|
5004
|
+
}
|
|
5005
|
+
if (pendingBlend?.key === key) {
|
|
5006
|
+
pendingBlend.position = request.position;
|
|
5007
|
+
return pendingBlend.promise;
|
|
5008
|
+
}
|
|
5009
|
+
const runId = ++requestId;
|
|
5010
|
+
options.onTransition?.({ currentTheme: current.theme, phase: "preloading", targetTheme });
|
|
5011
|
+
const pending = { key, position: request.position, promise: void 0 };
|
|
5012
|
+
pending.promise = prepareBlend(request.themes).then(
|
|
5013
|
+
(plan) => enqueue(runId, {
|
|
5014
|
+
key,
|
|
5015
|
+
kind: "blend",
|
|
5016
|
+
plan,
|
|
5017
|
+
position: () => pending.position,
|
|
5018
|
+
themes: request.themes,
|
|
5019
|
+
transition
|
|
5020
|
+
}),
|
|
5021
|
+
(error) => handlePreloadFailure(runId, targetTheme, error)
|
|
5022
|
+
).finally(() => {
|
|
5023
|
+
if (pendingBlend === pending) pendingBlend = void 0;
|
|
5024
|
+
});
|
|
5025
|
+
pendingBlend = pending;
|
|
5026
|
+
return pending.promise;
|
|
5027
|
+
},
|
|
5028
|
+
setTheme(style, changeOptions) {
|
|
4101
5029
|
const invalidTheme = validateConcreteRuntimeTheme(style, "target");
|
|
4102
5030
|
if (invalidTheme) {
|
|
4103
5031
|
return Promise.resolve({ error: invalidTheme, status: "failed", theme: style.theme });
|
|
4104
5032
|
}
|
|
4105
|
-
if (disposed)
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
theme: style.theme
|
|
4110
|
-
});
|
|
4111
|
-
}
|
|
5033
|
+
if (disposed) return Promise.resolve(disposedResult(style.theme));
|
|
5034
|
+
const transition = transitionOf(changeOptions);
|
|
5035
|
+
if (transition instanceof TypeError)
|
|
5036
|
+
return Promise.resolve({ error: transition, status: "failed", theme: style.theme });
|
|
4112
5037
|
const runId = ++requestId;
|
|
5038
|
+
pendingBlend = void 0;
|
|
4113
5039
|
options.onTransition?.({
|
|
4114
5040
|
currentTheme: current.theme,
|
|
4115
5041
|
phase: "preloading",
|
|
@@ -4119,18 +5045,54 @@ function createTileflowThemeController(options) {
|
|
|
4119
5045
|
try {
|
|
4120
5046
|
preloaded = loadFonts(style);
|
|
4121
5047
|
} catch (error) {
|
|
4122
|
-
return Promise.resolve(handlePreloadFailure(runId, style, error));
|
|
5048
|
+
return Promise.resolve(handlePreloadFailure(runId, style.theme, error));
|
|
4123
5049
|
}
|
|
4124
5050
|
return preloaded.then(
|
|
4125
|
-
() =>
|
|
4126
|
-
(error) => handlePreloadFailure(runId, style, error)
|
|
5051
|
+
() => enqueue(runId, { kind: "theme", style, transition }),
|
|
5052
|
+
(error) => handlePreloadFailure(runId, style.theme, error)
|
|
4127
5053
|
);
|
|
4128
5054
|
}
|
|
4129
5055
|
};
|
|
4130
|
-
function
|
|
5056
|
+
function transitionOf(changeOptions) {
|
|
5057
|
+
if (changeOptions?.transition === void 0) return defaultTransition;
|
|
5058
|
+
try {
|
|
5059
|
+
return normalizeThemeTransitionDuration(changeOptions.transition);
|
|
5060
|
+
} catch (error) {
|
|
5061
|
+
return error;
|
|
5062
|
+
}
|
|
5063
|
+
}
|
|
5064
|
+
function blendKey(themes) {
|
|
5065
|
+
return JSON.stringify(
|
|
5066
|
+
themes.map((theme) => {
|
|
5067
|
+
if (typeof theme.style === "string") return [theme.theme, theme.style];
|
|
5068
|
+
let identity = styleIdentities.get(theme.style);
|
|
5069
|
+
if (identity === void 0) {
|
|
5070
|
+
nextStyleIdentity += 1;
|
|
5071
|
+
identity = nextStyleIdentity;
|
|
5072
|
+
styleIdentities.set(theme.style, identity);
|
|
5073
|
+
}
|
|
5074
|
+
return [theme.theme, identity];
|
|
5075
|
+
})
|
|
5076
|
+
);
|
|
5077
|
+
}
|
|
5078
|
+
async function prepareBlend(themes) {
|
|
5079
|
+
if (!canBlendTileflowThemes(options.map)) {
|
|
5080
|
+
throw new TypeError("Tileflow theme blends require a MapLibre GL JS map.");
|
|
5081
|
+
}
|
|
5082
|
+
const [styles] = await Promise.all([
|
|
5083
|
+
Promise.all(
|
|
5084
|
+
themes.map(
|
|
5085
|
+
(theme) => typeof theme.style === "string" ? loadStyle(theme.style) : Promise.resolve(theme.style)
|
|
5086
|
+
)
|
|
5087
|
+
),
|
|
5088
|
+
Promise.all(themes.map((theme) => loadFonts(theme)))
|
|
5089
|
+
]);
|
|
5090
|
+
return planTileflowThemeBlend(styles);
|
|
5091
|
+
}
|
|
5092
|
+
function enqueue(runId, target) {
|
|
4131
5093
|
const operation = applyQueue.then(
|
|
4132
|
-
() =>
|
|
4133
|
-
() =>
|
|
5094
|
+
() => applyTarget(runId, target),
|
|
5095
|
+
() => applyTarget(runId, target)
|
|
4134
5096
|
);
|
|
4135
5097
|
applyQueue = operation.then(
|
|
4136
5098
|
() => void 0,
|
|
@@ -4138,59 +5100,160 @@ function createTileflowThemeController(options) {
|
|
|
4138
5100
|
);
|
|
4139
5101
|
return operation;
|
|
4140
5102
|
}
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
});
|
|
5103
|
+
function targetThemeOf(target) {
|
|
5104
|
+
return target.kind === "theme" ? target.style.theme : target.themes[Math.round(clampBlendPosition(target.position(), target.themes.length))]?.theme;
|
|
5105
|
+
}
|
|
5106
|
+
async function applyTarget(runId, target) {
|
|
5107
|
+
const targetTheme = targetThemeOf(target);
|
|
5108
|
+
if (disposed || runId !== requestId) return { status: "superseded", theme: targetTheme };
|
|
5109
|
+
const previous = { current, shown };
|
|
5110
|
+
options.onTransition?.({ currentTheme: previous.current.theme, phase: "applying", targetTheme });
|
|
5111
|
+
const cover = beginTileflowStyleCrossfade(options.map, target.transition);
|
|
5112
|
+
activeBlend?.blender.dispose();
|
|
5113
|
+
activeBlend = void 0;
|
|
5114
|
+
const styleInput = target.kind === "theme" ? target.style.style : target.plan.styleAt(target.position());
|
|
4149
5115
|
try {
|
|
4150
|
-
await applyMapStyle(options.map,
|
|
4151
|
-
if (disposed)
|
|
5116
|
+
await applyMapStyle(options.map, styleInput, timeoutMs);
|
|
5117
|
+
if (disposed) {
|
|
5118
|
+
cover?.cancel();
|
|
5119
|
+
return { status: "superseded", theme: targetTheme };
|
|
5120
|
+
}
|
|
4152
5121
|
if (runId !== requestId) {
|
|
5122
|
+
cover?.cancel();
|
|
4153
5123
|
try {
|
|
4154
|
-
await
|
|
5124
|
+
await restore(previous);
|
|
4155
5125
|
} catch {
|
|
4156
5126
|
}
|
|
4157
|
-
return { status: "superseded", theme:
|
|
5127
|
+
return { status: "superseded", theme: targetTheme };
|
|
5128
|
+
}
|
|
5129
|
+
if (target.kind === "theme") {
|
|
5130
|
+
current = target.style;
|
|
5131
|
+
shown = { style: styleInput };
|
|
5132
|
+
} else {
|
|
5133
|
+
const blender = createBlender(target.plan, target.position());
|
|
5134
|
+
activeBlend = { blender, key: target.key, plan: target.plan, themes: target.themes };
|
|
5135
|
+
shown = { blend: activeBlend, style: styleInput };
|
|
5136
|
+
current = target.themes[blender.dominant];
|
|
5137
|
+
if (pendingBlend?.key === target.key) pendingBlend = void 0;
|
|
4158
5138
|
}
|
|
4159
|
-
|
|
5139
|
+
await cover?.finish({ stop: () => disposed || runId !== requestId });
|
|
4160
5140
|
options.onTransition?.({
|
|
4161
|
-
currentTheme:
|
|
5141
|
+
currentTheme: current.theme,
|
|
4162
5142
|
phase: "ready",
|
|
4163
|
-
targetTheme:
|
|
5143
|
+
targetTheme: current.theme
|
|
4164
5144
|
});
|
|
4165
|
-
return { status: "applied", theme:
|
|
5145
|
+
return { status: "applied", theme: current.theme };
|
|
4166
5146
|
} catch (error) {
|
|
5147
|
+
cover?.cancel();
|
|
4167
5148
|
const normalized = normalizeThemeTransitionError(error, "Tileflow theme change failed.");
|
|
4168
5149
|
try {
|
|
4169
|
-
await
|
|
4170
|
-
current = previous;
|
|
5150
|
+
await restore(previous);
|
|
4171
5151
|
} catch {
|
|
4172
5152
|
}
|
|
4173
|
-
if (disposed || runId !== requestId) return { status: "superseded", theme:
|
|
5153
|
+
if (disposed || runId !== requestId) return { status: "superseded", theme: targetTheme };
|
|
4174
5154
|
options.onTransition?.({
|
|
4175
5155
|
currentTheme: current.theme,
|
|
4176
5156
|
error: normalized,
|
|
4177
5157
|
phase: "error",
|
|
4178
|
-
targetTheme
|
|
5158
|
+
targetTheme
|
|
4179
5159
|
});
|
|
4180
|
-
return { error: normalized, status: "failed", theme:
|
|
5160
|
+
return { error: normalized, status: "failed", theme: targetTheme };
|
|
5161
|
+
}
|
|
5162
|
+
}
|
|
5163
|
+
function createBlender(plan, position) {
|
|
5164
|
+
return createTileflowThemeBlender(options.map, plan, {
|
|
5165
|
+
cover(change) {
|
|
5166
|
+
const fade = beginTileflowStyleCrossfade(options.map, blendLabelFadeMs);
|
|
5167
|
+
change();
|
|
5168
|
+
void fade?.finish();
|
|
5169
|
+
},
|
|
5170
|
+
labelFadeMs: reducedMotion() ? 0 : blendLabelFadeMs,
|
|
5171
|
+
position
|
|
5172
|
+
});
|
|
5173
|
+
}
|
|
5174
|
+
async function restore(previous) {
|
|
5175
|
+
const blend = previous.shown.blend;
|
|
5176
|
+
if (blend) {
|
|
5177
|
+
const position = blend.blender.position;
|
|
5178
|
+
const style = blend.plan.styleAt(position);
|
|
5179
|
+
await applyMapStyle(options.map, style, timeoutMs);
|
|
5180
|
+
activeBlend = { ...blend, blender: createBlender(blend.plan, position) };
|
|
5181
|
+
shown = { blend: activeBlend, style };
|
|
5182
|
+
} else {
|
|
5183
|
+
await applyMapStyle(options.map, previous.shown.style, timeoutMs);
|
|
5184
|
+
shown = previous.shown;
|
|
4181
5185
|
}
|
|
5186
|
+
current = previous.current;
|
|
5187
|
+
}
|
|
5188
|
+
function reducedMotion() {
|
|
5189
|
+
const view = tileflowThemeMotionView(options.map);
|
|
5190
|
+
return view ? prefersReducedMotion(view) : false;
|
|
4182
5191
|
}
|
|
4183
|
-
function handlePreloadFailure(runId,
|
|
4184
|
-
if (disposed || runId !== requestId) return { status: "superseded", theme:
|
|
5192
|
+
function handlePreloadFailure(runId, targetTheme, error) {
|
|
5193
|
+
if (disposed || runId !== requestId) return { status: "superseded", theme: targetTheme };
|
|
4185
5194
|
const normalized = normalizeThemeTransitionError(error, "Tileflow theme preload failed.");
|
|
4186
5195
|
options.onTransition?.({
|
|
4187
5196
|
currentTheme: current.theme,
|
|
4188
5197
|
error: normalized,
|
|
4189
5198
|
phase: "error",
|
|
4190
|
-
targetTheme
|
|
5199
|
+
targetTheme
|
|
4191
5200
|
});
|
|
4192
|
-
return { error: normalized, status: "failed", theme:
|
|
5201
|
+
return { error: normalized, status: "failed", theme: targetTheme };
|
|
5202
|
+
}
|
|
5203
|
+
}
|
|
5204
|
+
function disposedResult(theme) {
|
|
5205
|
+
return { error: new Error("Tileflow theme controller is disposed."), status: "failed", theme };
|
|
5206
|
+
}
|
|
5207
|
+
function clampBlendPosition(position, themes) {
|
|
5208
|
+
return Math.min(themes - 1, Math.max(0, Number.isFinite(position) ? position : 0));
|
|
5209
|
+
}
|
|
5210
|
+
function nearestTheme(request) {
|
|
5211
|
+
if (!Array.isArray(request?.themes) || request.themes.length === 0) return void 0;
|
|
5212
|
+
return request.themes[Math.round(clampBlendPosition(request.position, request.themes.length))]?.theme;
|
|
5213
|
+
}
|
|
5214
|
+
function validateBlendRequest(request) {
|
|
5215
|
+
if (!request || !Array.isArray(request.themes) || request.themes.length < 2 || request.themes.length > maximumBlendThemes) {
|
|
5216
|
+
return new TypeError(
|
|
5217
|
+
`A Tileflow theme blend requires two to ${maximumBlendThemes} concrete themes.`
|
|
5218
|
+
);
|
|
5219
|
+
}
|
|
5220
|
+
for (const theme of request.themes) {
|
|
5221
|
+
const invalid = validateConcreteRuntimeTheme(theme, "target");
|
|
5222
|
+
if (invalid) return invalid;
|
|
5223
|
+
}
|
|
5224
|
+
if (typeof request.position !== "number" || !Number.isFinite(request.position) || request.position < 0 || request.position > request.themes.length - 1) {
|
|
5225
|
+
return new TypeError(
|
|
5226
|
+
`A Tileflow theme blend position must be a number from 0 to ${request.themes.length - 1}.`
|
|
5227
|
+
);
|
|
5228
|
+
}
|
|
5229
|
+
return void 0;
|
|
5230
|
+
}
|
|
5231
|
+
function normalizeThemeTransitionDuration(transition) {
|
|
5232
|
+
const duration = transition?.duration ?? 0;
|
|
5233
|
+
if (typeof duration !== "number" || !Number.isFinite(duration) || duration < 0 || duration > maximumTransitionMs) {
|
|
5234
|
+
throw new TypeError(
|
|
5235
|
+
`A Tileflow theme transition duration must be a number of milliseconds from 0 to ${maximumTransitionMs}.`
|
|
5236
|
+
);
|
|
5237
|
+
}
|
|
5238
|
+
return Math.round(duration);
|
|
5239
|
+
}
|
|
5240
|
+
async function loadTileflowBlendStyle(url) {
|
|
5241
|
+
const response = await globalThis.fetch(resolveTileflowBrowserResourceUrl(url), {
|
|
5242
|
+
cache: "default",
|
|
5243
|
+
credentials: "same-origin",
|
|
5244
|
+
redirect: "error"
|
|
5245
|
+
});
|
|
5246
|
+
if (!response.ok) throw new Error(`Tileflow theme style failed: ${response.status}`);
|
|
5247
|
+
const bytes = await readBoundedTileflowBrowserResource(
|
|
5248
|
+
response,
|
|
5249
|
+
maximumTileflowBlendStyleBytes,
|
|
5250
|
+
"Tileflow theme style"
|
|
5251
|
+
);
|
|
5252
|
+
const style = JSON.parse(new TextDecoder().decode(bytes));
|
|
5253
|
+
if (!style || typeof style !== "object" || !Array.isArray(style.layers)) {
|
|
5254
|
+
throw new Error("Tileflow theme style is not a MapLibre style document.");
|
|
4193
5255
|
}
|
|
5256
|
+
return style;
|
|
4194
5257
|
}
|
|
4195
5258
|
function assertConcreteRuntimeTheme(style, role) {
|
|
4196
5259
|
const error = validateConcreteRuntimeTheme(style, role);
|