@tileflow/core 0.1.0-alpha.30 → 0.1.0-alpha.32

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/dist/browser.js CHANGED
@@ -2042,10 +2042,231 @@ var init_dist = __esm({
2042
2042
  }
2043
2043
  });
2044
2044
 
2045
+ // src/dynamic-style-images.ts
2046
+ var owners = /* @__PURE__ */ new WeakMap();
2047
+ function isRestoredCopy(image, previous) {
2048
+ if (!image || !previous) return false;
2049
+ const a = image, b2 = previous;
2050
+ if (!a.data || !b2.data || a.data.width !== b2.data.width || a.data.height !== b2.data.height || a.pixelRatio !== b2.pixelRatio || a.sdf !== b2.sdf || a.content !== b2.content || a.stretchX !== b2.stretchX || a.stretchY !== b2.stretchY || a.data.data.length !== b2.data.data.length)
2051
+ return false;
2052
+ return a.data.data.every((byte, index) => byte === b2.data.data[index]);
2053
+ }
2054
+ function createTileflowDynamicImageRegistry(input, context, beforeRestore) {
2055
+ for (const method of [
2056
+ "addImage",
2057
+ "getImage",
2058
+ "getStyle",
2059
+ "removeImage",
2060
+ "updateImage"
2061
+ ]) {
2062
+ if (typeof input[method] !== "function") {
2063
+ throw new TypeError(`Dynamic images require a map with ${method}().`);
2064
+ }
2065
+ }
2066
+ const map = input;
2067
+ const names = owners.get(map) ?? /* @__PURE__ */ new Set();
2068
+ owners.set(map, names);
2069
+ const entries = /* @__PURE__ */ new Map();
2070
+ const view = typeof map.getCanvas === "function" ? map.getCanvas().ownerDocument?.defaultView : void 0;
2071
+ let cancelFrame;
2072
+ let disposed = false;
2073
+ let suspended = false;
2074
+ const imageContext = (entry, current) => Object.freeze({
2075
+ ...current,
2076
+ width: entry.pixels.width,
2077
+ height: entry.pixels.height,
2078
+ pixelRatio: entry.pixelRatio
2079
+ });
2080
+ const keyOf = (entry, current) => JSON.stringify([
2081
+ current.theme,
2082
+ current.blend,
2083
+ (entry.definition.states ?? []).map((name) => [
2084
+ name,
2085
+ typeof current.states[name],
2086
+ current.states[name]
2087
+ ])
2088
+ ]);
2089
+ const report = (entry, cause) => {
2090
+ const error = cause instanceof Error ? cause : new Error(String(cause));
2091
+ if (entry.definition.onError) entry.definition.onError(error);
2092
+ else console.error(`Tileflow dynamic image "${entry.name}":`, error);
2093
+ };
2094
+ function render(entry, current) {
2095
+ const output = entry.definition.render(imageContext(entry, current));
2096
+ if (output === void 0) return false;
2097
+ if (!(output instanceof Uint8Array || output instanceof Uint8ClampedArray) || output.byteLength !== entry.pixels.width * entry.pixels.height * 4) {
2098
+ throw new TypeError(
2099
+ `Dynamic image "${entry.name}" must return width \xD7 height \xD7 4 RGBA bytes.`
2100
+ );
2101
+ }
2102
+ entry.pixels = { ...entry.pixels, data: Uint8Array.from(output) };
2103
+ return true;
2104
+ }
2105
+ function install(entry, changed = false) {
2106
+ if (suspended) return;
2107
+ const existing = map.getImage(entry.name);
2108
+ if (!existing && !map.getStyle()) return;
2109
+ if (existing && existing !== entry.installed && !isRestoredCopy(existing, entry.recoveryImage)) {
2110
+ throw new Error(`Dynamic image "${entry.name}" conflicts with an existing image owner.`);
2111
+ }
2112
+ if (!existing) map.addImage(entry.name, entry.pixels, { pixelRatio: entry.pixelRatio });
2113
+ else if (changed) map.updateImage(entry.name, entry.pixels);
2114
+ entry.installed = map.getImage(entry.name);
2115
+ entry.recoveryImage = void 0;
2116
+ }
2117
+ function flush() {
2118
+ cancelFrame = void 0;
2119
+ if (disposed || suspended) return;
2120
+ const current = context();
2121
+ for (const entry of entries.values()) {
2122
+ if (!entry.dirty) continue;
2123
+ entry.dirty = false;
2124
+ entry.key = keyOf(entry, current);
2125
+ try {
2126
+ const changed = render(entry, current);
2127
+ install(entry, changed);
2128
+ } catch (error) {
2129
+ report(entry, error);
2130
+ }
2131
+ }
2132
+ }
2133
+ function schedule() {
2134
+ if (cancelFrame || disposed || suspended) return;
2135
+ if (view?.requestAnimationFrame) {
2136
+ const id = view.requestAnimationFrame(flush);
2137
+ cancelFrame = () => view.cancelAnimationFrame(id);
2138
+ } else {
2139
+ let cancelled = false;
2140
+ cancelFrame = () => {
2141
+ cancelled = true;
2142
+ };
2143
+ queueMicrotask(() => {
2144
+ if (!cancelled) flush();
2145
+ });
2146
+ }
2147
+ }
2148
+ function restore() {
2149
+ if (disposed || suspended) return;
2150
+ beforeRestore();
2151
+ for (const entry of entries.values()) {
2152
+ try {
2153
+ install(entry, true);
2154
+ } catch (error) {
2155
+ report(entry, error);
2156
+ }
2157
+ }
2158
+ if ([...entries.values()].some((entry) => entry.dirty)) schedule();
2159
+ }
2160
+ function loseContext() {
2161
+ suspended = true;
2162
+ for (const entry of entries.values()) entry.recoveryImage = entry.installed;
2163
+ cancelFrame?.();
2164
+ cancelFrame = void 0;
2165
+ }
2166
+ function restoreContext() {
2167
+ suspended = false;
2168
+ restore();
2169
+ }
2170
+ function release(entry, remove) {
2171
+ if (entries.get(entry.name) !== entry) return;
2172
+ entries.delete(entry.name);
2173
+ names.delete(entry.name);
2174
+ if (remove && entry.installed && map.getImage(entry.name) === entry.installed) {
2175
+ map.removeImage(entry.name);
2176
+ }
2177
+ if (!entries.size) {
2178
+ cancelFrame?.();
2179
+ cancelFrame = void 0;
2180
+ }
2181
+ }
2182
+ function dispose(remove = true) {
2183
+ if (disposed) return;
2184
+ disposed = true;
2185
+ cancelFrame?.();
2186
+ cancelFrame = void 0;
2187
+ for (const entry of [...entries.values()]) release(entry, remove);
2188
+ map.off("style.load", restore);
2189
+ map.off("webglcontextlost", loseContext);
2190
+ map.off("webglcontextrestored", restoreContext);
2191
+ map.off("remove", removed);
2192
+ }
2193
+ const removed = () => dispose(false);
2194
+ map.on("style.load", restore);
2195
+ map.on("webglcontextlost", loseContext);
2196
+ map.on("webglcontextrestored", restoreContext);
2197
+ map.on("remove", removed);
2198
+ return {
2199
+ dispose,
2200
+ assertBlend(images) {
2201
+ for (const image of images) {
2202
+ if (image.names.some((name) => entries.has(name))) {
2203
+ throw new TypeError("Dynamic image names must remain identical across blended themes.");
2204
+ }
2205
+ }
2206
+ },
2207
+ invalidate(state) {
2208
+ if (disposed) return;
2209
+ const current = context();
2210
+ for (const entry of entries.values()) {
2211
+ if (state !== void 0 && !entry.definition.states?.includes(state)) continue;
2212
+ if (keyOf(entry, current) === entry.key) continue;
2213
+ entry.dirty = true;
2214
+ }
2215
+ if ([...entries.values()].some((entry) => entry.dirty)) schedule();
2216
+ },
2217
+ register(name, definition) {
2218
+ if (disposed) throw new Error("Dynamic image registry is disposed.");
2219
+ if (typeof name !== "string" || !name.trim())
2220
+ throw new TypeError("Dynamic image names must be non-empty.");
2221
+ if (names.has(name))
2222
+ throw new Error(`Dynamic image "${name}" already has a registered owner.`);
2223
+ if (map.getImage(name)) throw new Error(`Dynamic image "${name}" already exists.`);
2224
+ const { width, height, pixelRatio = 1 } = definition;
2225
+ if (![width, height].every((value) => Number.isInteger(value) && value > 0 && value <= 2048) || !Number.isFinite(pixelRatio) || pixelRatio <= 0 || typeof definition.render !== "function" || definition.states !== void 0 && (!Array.isArray(definition.states) || definition.states.some(
2226
+ (state) => typeof state !== "string" || !state.trim() || state !== state.trim()
2227
+ ))) {
2228
+ throw new TypeError(
2229
+ "Dynamic images require fixed dimensions, a positive pixel ratio, valid state names and a render function."
2230
+ );
2231
+ }
2232
+ const entry = {
2233
+ name,
2234
+ definition: { ...definition, states: definition.states?.slice() },
2235
+ pixelRatio,
2236
+ pixels: { width, height, data: new Uint8Array(width * height * 4) },
2237
+ dirty: false,
2238
+ key: ""
2239
+ };
2240
+ names.add(name);
2241
+ try {
2242
+ const current = context();
2243
+ entry.key = keyOf(entry, current);
2244
+ render(entry, current);
2245
+ install(entry);
2246
+ entries.set(name, entry);
2247
+ } catch (error) {
2248
+ names.delete(name);
2249
+ throw error;
2250
+ }
2251
+ return {
2252
+ refresh() {
2253
+ if (disposed || entries.get(name) !== entry) return;
2254
+ entry.dirty = true;
2255
+ schedule();
2256
+ },
2257
+ dispose() {
2258
+ release(entry, !disposed);
2259
+ }
2260
+ };
2261
+ }
2262
+ };
2263
+ }
2264
+
2045
2265
  // src/browser.ts
2046
2266
  init_portable_identity_rules();
2047
2267
 
2048
2268
  // src/runtime.ts
2269
+ init_portable_identity_rules();
2049
2270
  var defaultTileflowRuntimeView = Object.freeze({
2050
2271
  bearing: 0,
2051
2272
  center: [0, 20],
@@ -2066,6 +2287,11 @@ var maximumManifestBytes = 1024 * 1024;
2066
2287
  var maximumSessionGrantBytes = 64 * 1024;
2067
2288
  var manifestCache = new globalThis.Map();
2068
2289
  var tileflowThemeBlendLimits = Object.freeze({ maximumThemes: 8, minimumThemes: 2 });
2290
+ function validateTileflowThemeBlendSelection(value) {
2291
+ if (!value || typeof value !== "object") return false;
2292
+ const { position, themes } = value;
2293
+ return Array.isArray(themes) && themes.length >= tileflowThemeBlendLimits.minimumThemes && themes.length <= tileflowThemeBlendLimits.maximumThemes && themes.every((theme) => isTileflowThemeName(theme)) && typeof position === "number" && Number.isFinite(position) && position >= 0 && position <= themes.length - 1;
2294
+ }
2069
2295
  function getTileflowStyleFontFaces(style) {
2070
2296
  const input = style.metadata?.[tileflowStyleFontFacesMetadataKey];
2071
2297
  if (input === void 0) return [];
@@ -2238,17 +2464,21 @@ var featureOperators = /* @__PURE__ */ new Set([
2238
2464
  "properties"
2239
2465
  ]);
2240
2466
  var maximumBranches = 24;
2241
- function planTileflowThemeBlend(styles) {
2467
+ function planTileflowThemeBlend(styles, options = {}) {
2242
2468
  if (styles.length < 2)
2243
2469
  throw new TileflowThemeBlendError("A theme blend needs two or more themes.");
2244
2470
  const [base] = styles;
2245
2471
  assertSameStructure(styles, base);
2472
+ const mixedImages = new Set(imageProperties);
2473
+ if (options.iconImages === "switch") mixedImages.delete("icon-image");
2246
2474
  const paints = [];
2247
2475
  const switches = [];
2248
- const imageGroups = collectImageNames(styles, base);
2476
+ const imageGroups = collectImageNames(styles, base, mixedImages);
2477
+ const symbolLayers = /* @__PURE__ */ new Set();
2249
2478
  const images = /* @__PURE__ */ new Map();
2250
2479
  let splitLayers = 0;
2251
2480
  let copiedLayers = 0;
2481
+ let switchedLayers = 0;
2252
2482
  const layers = [];
2253
2483
  for (const [index, layer] of base.layers.entries()) {
2254
2484
  const variants = styles.map((style) => style.layers[index]);
@@ -2262,6 +2492,7 @@ function planTileflowThemeBlend(styles) {
2262
2492
  }
2263
2493
  const sky = differingRoot(styles, "sky");
2264
2494
  const light = differingRoot(styles, "light");
2495
+ const metadata = alignInteractionManifest(base.metadata, layers);
2265
2496
  return {
2266
2497
  themes: styles.length,
2267
2498
  paints,
@@ -2269,14 +2500,21 @@ function planTileflowThemeBlend(styles) {
2269
2500
  switches,
2270
2501
  ...sky === void 0 ? {} : { sky },
2271
2502
  ...light === void 0 ? {} : { light },
2272
- summary: { splitLayers, copiedLayers, switchedValues: switches.length },
2503
+ summary: { splitLayers, copiedLayers, switchedLayers, switchedValues: switches.length },
2273
2504
  styleAt(position) {
2274
2505
  const at = clampPosition(position, styles.length);
2275
2506
  const dominant = Math.round(at);
2276
2507
  const style = {
2277
2508
  ...base,
2509
+ ...metadata === void 0 ? {} : { metadata },
2510
+ // Symbols show the nearest theme, as the label cross-fade leaves them between changes.
2278
2511
  layers: layers.map(
2279
- (layer) => fillLayer(layer, at, dominant, switchesByLayer.get(String(layer.id)))
2512
+ (layer) => fillLayer(
2513
+ layer,
2514
+ symbolLayers.has(String(layer.id)) ? dominant : at,
2515
+ dominant,
2516
+ switchesByLayer.get(String(layer.id))
2517
+ )
2280
2518
  )
2281
2519
  };
2282
2520
  if (sky !== void 0) style.sky = fillTemplate(sky, at);
@@ -2301,7 +2539,7 @@ function planTileflowThemeBlend(styles) {
2301
2539
  (variant) => variant[group]?.[property]
2302
2540
  );
2303
2541
  if (values.every((value) => same(value, values[0]))) continue;
2304
- if (imageProperties.has(property)) {
2542
+ if (mixedImages.has(property)) {
2305
2543
  const tuples = imageTuples(values);
2306
2544
  if (tuples && tuples.every((names) => imageGroups.get(names[0])?.size === 1)) {
2307
2545
  for (const names of tuples) {
@@ -2345,17 +2583,46 @@ function planTileflowThemeBlend(styles) {
2345
2583
  values: variants.map((variant) => paintOf(variant)[property])
2346
2584
  });
2347
2585
  }
2586
+ if (options.featureSwitches === "layers" && results.length === 1 && ownSwitches.some((entry) => entry.values.some(isDataDriven)))
2587
+ results = switchLayer();
2348
2588
  for (const result of results) {
2349
2589
  const resultId = String(result.layer.id);
2590
+ if (type === "symbol") symbolLayers.add(resultId);
2350
2591
  const overridden = new Set(result.extra.map((entry) => entry.property));
2351
2592
  for (const entry of [
2352
2593
  ...ownPaints.filter((own) => !overridden.has(own.property)),
2353
2594
  ...result.extra
2354
2595
  ])
2355
2596
  paints.push({ layer: resultId, property: entry.property, template: entry.template, follows });
2356
- for (const entry of ownSwitches) switches.push({ layer: resultId, ...entry });
2597
+ if (result.visibility)
2598
+ switches.push({
2599
+ layer: resultId,
2600
+ group: "layout",
2601
+ property: "visibility",
2602
+ values: result.visibility
2603
+ });
2604
+ else for (const entry of ownSwitches) switches.push({ layer: resultId, ...entry });
2357
2605
  }
2358
2606
  return results.map((result) => result.layer);
2607
+ function switchLayer() {
2608
+ switchedLayers += variants.length;
2609
+ return variants.map((_2, theme) => {
2610
+ const copyPaint = { ...paint };
2611
+ const copyLayout = { ...layout };
2612
+ for (const entry of ownSwitches) {
2613
+ const target = entry.group === "paint" ? copyPaint : copyLayout;
2614
+ const value = entry.values[theme];
2615
+ if (value === void 0) delete target[entry.property];
2616
+ else target[entry.property] = value;
2617
+ }
2618
+ const shown = copyLayout.visibility === "none" ? "none" : "visible";
2619
+ return {
2620
+ layer: { ...planned, id: `${id}::theme-${theme}`, paint: copyPaint, layout: copyLayout },
2621
+ extra: [],
2622
+ visibility: variants.map((__, nearest) => nearest === theme ? shown : "none")
2623
+ };
2624
+ });
2625
+ }
2359
2626
  function splitLayer() {
2360
2627
  if (isLegacyFilter(layer.filter)) return void 0;
2361
2628
  const branches = splitBranches(
@@ -2420,6 +2687,15 @@ function planTileflowThemeBlend(styles) {
2420
2687
  function fillTileflowThemeBlendTemplate(template, position) {
2421
2688
  return fillTemplate(template, position);
2422
2689
  }
2690
+ function mixTileflowColours(from, to, t) {
2691
+ const a = oklab(from);
2692
+ const b2 = oklab(to);
2693
+ if (!a || !b2) return void 0;
2694
+ return colourFromOklab(a.map((value, channel) => value + (b2[channel] - value) * t));
2695
+ }
2696
+ function readsTileflowFeatureData(value) {
2697
+ return isDataDriven(value) || reads(value, "within") || reads(value, "distance");
2698
+ }
2423
2699
  function fillLayer(layer, position, dominant, layerSwitches) {
2424
2700
  const filled = { ...layer };
2425
2701
  for (const group of ["layout", "paint"]) {
@@ -2504,11 +2780,11 @@ function differingRoot(styles, key) {
2504
2780
  if (values.every((value) => same(value, values[0]))) return void 0;
2505
2781
  return tryMixTemplate(values);
2506
2782
  }
2507
- function collectImageNames(styles, base) {
2783
+ function collectImageNames(styles, base, properties) {
2508
2784
  const uses = /* @__PURE__ */ new Map();
2509
2785
  for (const [index] of base.layers.entries()) {
2510
2786
  for (const group of ["layout", "paint"]) {
2511
- for (const property of imageProperties) {
2787
+ for (const property of properties) {
2512
2788
  const values = styles.map(
2513
2789
  (style) => style.layers[index]?.[group]?.[property]
2514
2790
  );
@@ -2523,6 +2799,51 @@ function collectImageNames(styles, base) {
2523
2799
  }
2524
2800
  return uses;
2525
2801
  }
2802
+ function alignInteractionManifest(metadata, layers) {
2803
+ if (!isRecord2(metadata)) return void 0;
2804
+ const planned = /* @__PURE__ */ new Map();
2805
+ for (const [index, layer] of layers.entries()) {
2806
+ const id = String(layer.id);
2807
+ const original = /^(.+)::(?:branch|theme)-\d+$/u.exec(id)?.[1] ?? id;
2808
+ planned.set(original, [...planned.get(original) ?? [], { id, index }]);
2809
+ }
2810
+ let aligned;
2811
+ const interactions = metadata["tileflow:interaction-manifest"];
2812
+ const domains = isRecord2(interactions) ? interactions.domains : void 0;
2813
+ if (isRecord2(interactions) && isRecord2(domains) && isRecord2(domains.poi)) {
2814
+ const poi = domains.poi;
2815
+ if (Array.isArray(poi.layers)) {
2816
+ const poiLayers = poi.layers.flatMap((entry) => {
2817
+ if (!isRecord2(entry) || typeof entry.layerId !== "string") return [entry];
2818
+ const copies = planned.get(entry.layerId);
2819
+ if (!copies) return [entry];
2820
+ return copies.map(({ id, index }) => ({ ...entry, layerId: id, priority: index }));
2821
+ });
2822
+ aligned = {
2823
+ ...metadata,
2824
+ "tileflow:interaction-manifest": {
2825
+ ...interactions,
2826
+ domains: { ...domains, poi: { ...poi, layers: poiLayers } }
2827
+ }
2828
+ };
2829
+ }
2830
+ }
2831
+ const overlays = metadata["tileflow:overlay-placement-manifest"];
2832
+ if (isRecord2(overlays) && isRecord2(overlays.anchors)) {
2833
+ const anchors = Object.fromEntries(
2834
+ Object.entries(overlays.anchors).map(([placement, anchor]) => [
2835
+ placement,
2836
+ typeof anchor === "string" ? planned.get(anchor)?.[0]?.id ?? anchor : anchor
2837
+ ])
2838
+ );
2839
+ aligned = {
2840
+ ...aligned ?? metadata,
2841
+ "tileflow:overlay-placement-manifest": { ...overlays, anchors }
2842
+ };
2843
+ }
2844
+ return aligned;
2845
+ }
2846
+ var isRecord2 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
2526
2847
  function imageTuples(values, out = []) {
2527
2848
  const [first] = values;
2528
2849
  if (values.every((value) => typeof value === "string")) {
@@ -2789,6 +3110,196 @@ function parseUnit(part, scale) {
2789
3110
  return percentage ? value / 100 * scale : value;
2790
3111
  }
2791
3112
 
3113
+ // src/layer-domain.ts
3114
+ var tileflowLayerDomainMetadataKey = "tileflow:domain";
3115
+
3116
+ // src/theme-emphasis.ts
3117
+ var colourProperties = {
3118
+ circle: ["circle-color", "circle-stroke-color"],
3119
+ fill: ["fill-color", "fill-outline-color"],
3120
+ "fill-extrusion": ["fill-extrusion-color"],
3121
+ line: ["line-color"]
3122
+ };
3123
+ var patternProperties = {
3124
+ fill: "fill-pattern",
3125
+ "fill-extrusion": "fill-extrusion-pattern",
3126
+ line: "line-pattern"
3127
+ };
3128
+ var opacityProperties2 = {
3129
+ circle: "circle-opacity",
3130
+ fill: "fill-opacity",
3131
+ "fill-extrusion": "fill-extrusion-opacity",
3132
+ heatmap: "heatmap-opacity",
3133
+ line: "line-opacity",
3134
+ raster: "raster-opacity"
3135
+ };
3136
+ var labelProperties = ["text-opacity", "icon-opacity"];
3137
+ var maximumAreaPositions = 4096;
3138
+ function validateTileflowEmphasis(value) {
3139
+ if (!isRecord3(value)) return new TypeError("A Tileflow emphasis must be an object.");
3140
+ for (const key of Object.keys(value)) {
3141
+ if (key !== "recede" && key !== "labels" && key !== "keep") {
3142
+ return new TypeError(`A Tileflow emphasis has no ${JSON.stringify(key)} option.`);
3143
+ }
3144
+ }
3145
+ if (value.recede !== void 0) {
3146
+ if (!isRecord3(value.recede)) {
3147
+ return new TypeError("A Tileflow emphasis recede must map module names to amounts.");
3148
+ }
3149
+ for (const [module, amount] of Object.entries(value.recede)) {
3150
+ if (!/^[a-z][a-z0-9-]*$/u.test(module) || !isUnit(amount)) {
3151
+ return new TypeError(
3152
+ `A Tileflow emphasis recede amount for ${JSON.stringify(module)} must be a number from 0 to 1.`
3153
+ );
3154
+ }
3155
+ }
3156
+ }
3157
+ if (value.labels !== void 0 && !isUnit(value.labels)) {
3158
+ return new TypeError("A Tileflow emphasis labels share must be a number from 0 to 1.");
3159
+ }
3160
+ if (value.keep !== void 0) {
3161
+ const positions = areaPositions(value.keep);
3162
+ if (positions === void 0) {
3163
+ return new TypeError(
3164
+ "A Tileflow emphasis keep area must be a GeoJSON Polygon or MultiPolygon."
3165
+ );
3166
+ }
3167
+ if (positions > maximumAreaPositions) {
3168
+ return new TypeError(
3169
+ `A Tileflow emphasis keep area may hold at most ${maximumAreaPositions} positions.`
3170
+ );
3171
+ }
3172
+ }
3173
+ return void 0;
3174
+ }
3175
+ function planTileflowEmphasis(layers, emphasis) {
3176
+ const targets = [];
3177
+ const fadeLabels = emphasis.labels === void 0 ? 0 : 1 - emphasis.labels;
3178
+ for (const layer of layers) {
3179
+ if (!isRecord3(layer) || typeof layer.id !== "string" || typeof layer.type !== "string")
3180
+ continue;
3181
+ const domain = isRecord3(layer.metadata) ? layer.metadata[tileflowLayerDomainMetadataKey] : void 0;
3182
+ if (typeof domain !== "string") continue;
3183
+ const paint = isRecord3(layer.paint) ? layer.paint : {};
3184
+ const add = (property, kind, amount2) => targets.push({
3185
+ amount: amount2,
3186
+ featureData: readsTileflowFeatureData(paint[property]) || kind === "label" && emphasis.keep !== void 0,
3187
+ kind,
3188
+ layer: layer.id,
3189
+ property
3190
+ });
3191
+ if (layer.type === "symbol") {
3192
+ if (fadeLabels > 0)
3193
+ for (const property of labelProperties) add(property, "label", fadeLabels);
3194
+ continue;
3195
+ }
3196
+ if (layer.type === "background") continue;
3197
+ const amount = emphasis.recede?.[domain] ?? 0;
3198
+ if (amount <= 0) continue;
3199
+ const pattern = patternProperties[layer.type];
3200
+ const opacity = opacityProperties2[layer.type];
3201
+ if (pattern && paint[pattern] !== void 0) {
3202
+ if (opacity) add(opacity, "fade", amount);
3203
+ continue;
3204
+ }
3205
+ const colours = (colourProperties[layer.type] ?? []).filter(
3206
+ (property) => paint[property] !== void 0
3207
+ );
3208
+ if (colours.length > 0) for (const property of colours) add(property, "colour", amount);
3209
+ else if (opacity) add(opacity, "fade", amount);
3210
+ }
3211
+ return targets;
3212
+ }
3213
+ function emphasizeTileflowValue(target, value, options) {
3214
+ const amount = target.amount * Math.min(1, Math.max(0, options.strength));
3215
+ if (target.kind === "label") {
3216
+ const keep = options.keep;
3217
+ if (amount <= 0 && !keep) return value;
3218
+ const near = keep ? (original, scaled) => ["case", ["within", keep], original, scaled] : void 0;
3219
+ return scaleOpacity(value, 1 - amount, near);
3220
+ }
3221
+ if (amount <= 0) return value;
3222
+ if (target.kind === "fade") return scaleOpacity(value, 1 - amount);
3223
+ if (options.ground === void 0) return value;
3224
+ return mixColours(value, options.ground, amount);
3225
+ }
3226
+ function tileflowEmphasisGround(layers) {
3227
+ for (const layer of layers) {
3228
+ if (!isRecord3(layer) || layer.type !== "background") continue;
3229
+ const colour = isRecord3(layer.paint) ? layer.paint["background-color"] : void 0;
3230
+ return firstColour(colour);
3231
+ }
3232
+ return void 0;
3233
+ }
3234
+ function firstColour(value) {
3235
+ if (typeof value === "string") {
3236
+ return mixTileflowColours(value, value, 0) === void 0 ? void 0 : value;
3237
+ }
3238
+ if (Array.isArray(value)) {
3239
+ for (const item of value.slice(1)) {
3240
+ const found = firstColour(item);
3241
+ if (found !== void 0) return found;
3242
+ }
3243
+ }
3244
+ return void 0;
3245
+ }
3246
+ function mixColours(value, toward, amount) {
3247
+ if (typeof value === "string") return mixTileflowColours(value, toward, amount) ?? value;
3248
+ if (Array.isArray(value)) {
3249
+ if (value[0] === "literal") return value;
3250
+ return value.map((item, index) => index === 0 ? item : mixColours(item, toward, amount));
3251
+ }
3252
+ return value;
3253
+ }
3254
+ var isZoomInput = (input) => Array.isArray(input) && input[0] === "zoom";
3255
+ function scaleOpacity(value, factor, near = (_original, scaled) => scaled) {
3256
+ const scale = (item) => scaleOpacity(item, factor, near);
3257
+ if (value === void 0 || value === null) return near(1, factor);
3258
+ if (typeof value === "number") return near(value, round(value * factor));
3259
+ if (Array.isArray(value)) {
3260
+ const operator = value[0];
3261
+ if (typeof operator === "string" && operator.startsWith("interpolate") && isZoomInput(value[2])) {
3262
+ return [
3263
+ ...value.slice(0, 3),
3264
+ ...value.slice(3).map((item, index) => index % 2 === 1 ? scale(item) : item)
3265
+ ];
3266
+ }
3267
+ if (operator === "step" && isZoomInput(value[1])) {
3268
+ return [
3269
+ "step",
3270
+ value[1],
3271
+ scale(value[2]),
3272
+ ...value.slice(3).map((item, index) => index % 2 === 1 ? scale(item) : item)
3273
+ ];
3274
+ }
3275
+ if (operator === "let") return [...value.slice(0, -1), scale(value.at(-1))];
3276
+ }
3277
+ return near(value, ["*", value, factor]);
3278
+ }
3279
+ var round = (value) => Math.round(value * 1e4) / 1e4;
3280
+ function areaPositions(value) {
3281
+ if (!isRecord3(value) || !Array.isArray(value.coordinates)) return void 0;
3282
+ const polygons = value.type === "Polygon" ? [value.coordinates] : value.type === "MultiPolygon" ? value.coordinates : void 0;
3283
+ if (!polygons) return void 0;
3284
+ let positions = 0;
3285
+ for (const polygon of polygons) {
3286
+ if (!Array.isArray(polygon) || polygon.length === 0) return void 0;
3287
+ for (const ring of polygon) {
3288
+ if (!Array.isArray(ring) || ring.length < 4) return void 0;
3289
+ for (const position of ring) {
3290
+ if (!Array.isArray(position) || position.length < 2 || !position.every(
3291
+ (coordinate) => typeof coordinate === "number" && Number.isFinite(coordinate)
3292
+ ))
3293
+ return void 0;
3294
+ positions += 1;
3295
+ }
3296
+ }
3297
+ }
3298
+ return positions;
3299
+ }
3300
+ var isUnit = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
3301
+ var isRecord3 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
3302
+
2792
3303
  // src/theme-motion-browser.ts
2793
3304
  var settleFrames = 3;
2794
3305
  var settleTimeoutMs = 1e4;
@@ -2896,18 +3407,27 @@ function createTileflowThemeBlender(input, plan, options) {
2896
3407
  const positionPaints = plan.paints.filter((paint) => paint.follows === "position");
2897
3408
  const dominantPaints = plan.paints.filter((paint) => paint.follows === "dominant");
2898
3409
  const pixels = createImageMixer(map, plan.images);
3410
+ const mixesArtwork = plan.images.some((image) => image.follows === "position");
3411
+ const relayoutAt = relayoutPositions(plan);
2899
3412
  const shownValues = /* @__PURE__ */ new Map();
3413
+ const baseValues = /* @__PURE__ */ new Map();
3414
+ const valueKey = (layer, property) => `${layer}\0${property}`;
3415
+ const transformed = (layer, property, value) => options.transform ? options.transform(layer, property, value) : value;
2900
3416
  let position = clamp(options.position);
2901
3417
  let dominant = Math.round(position);
2902
3418
  let label = dominant;
2903
3419
  let fade;
2904
3420
  let frameRequest;
2905
3421
  let disposed = false;
3422
+ let holding = false;
3423
+ let artworkWaits = false;
2906
3424
  pixels.paint("position", position);
2907
3425
  pixels.paint("dominant", label);
2908
3426
  const drive = (paints, at) => {
2909
3427
  for (const paint of paints) {
2910
- const value = fillTileflowThemeBlendTemplate(paint.template, at);
3428
+ const base = fillTileflowThemeBlendTemplate(paint.template, at);
3429
+ baseValues.set(valueKey(paint.layer, paint.property), base);
3430
+ const value = transformed(paint.layer, paint.property, base);
2911
3431
  const key = JSON.stringify(value);
2912
3432
  if (shownValues.get(paint) === key) continue;
2913
3433
  shownValues.set(paint, key);
@@ -2932,18 +3452,53 @@ function createTileflowThemeBlender(input, plan, options) {
2932
3452
  const applySwitches = (theme) => {
2933
3453
  for (const entry of plan.switches) {
2934
3454
  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 });
3455
+ if (entry.group === "paint") {
3456
+ baseValues.set(valueKey(entry.layer, entry.property), value);
3457
+ map.setPaintProperty(
3458
+ entry.layer,
3459
+ entry.property,
3460
+ transformed(entry.layer, entry.property, value),
3461
+ { validate: false }
3462
+ );
3463
+ } else map.setLayoutProperty(entry.layer, entry.property, value, { validate: false });
2938
3464
  }
2939
3465
  };
3466
+ const switchedPaints = plan.switches.filter((entry) => entry.group === "paint");
2940
3467
  return {
3468
+ baseValue(layer, property) {
3469
+ return baseValues.get(valueKey(layer, property));
3470
+ },
2941
3471
  get dominant() {
2942
3472
  return dominant;
2943
3473
  },
3474
+ get held() {
3475
+ return holding && (artworkWaits || Math.round(position) !== dominant);
3476
+ },
2944
3477
  get position() {
2945
3478
  return position;
2946
3479
  },
3480
+ relayoutAt,
3481
+ refresh(include) {
3482
+ if (disposed) return;
3483
+ drive(
3484
+ positionPaints.filter((paint) => include(paint.layer, paint.property)),
3485
+ position
3486
+ );
3487
+ drive(
3488
+ dominantPaints.filter((paint) => include(paint.layer, paint.property)),
3489
+ label
3490
+ );
3491
+ for (const entry of switchedPaints) {
3492
+ if (!include(entry.layer, entry.property)) continue;
3493
+ const value = entry.values[dominant];
3494
+ map.setPaintProperty(
3495
+ entry.layer,
3496
+ entry.property,
3497
+ transformed(entry.layer, entry.property, value),
3498
+ { validate: false }
3499
+ );
3500
+ }
3501
+ },
2947
3502
  dispose() {
2948
3503
  disposed = true;
2949
3504
  if (frameRequest !== void 0 && view) {
@@ -2953,21 +3508,30 @@ function createTileflowThemeBlender(input, plan, options) {
2953
3508
  }
2954
3509
  frameRequest = void 0;
2955
3510
  },
2956
- set(next) {
3511
+ set(next, setOptions) {
2957
3512
  if (disposed) return;
3513
+ const hold = setOptions?.hold === true;
3514
+ if (hold) holding = true;
2958
3515
  const target = clamp(next);
2959
3516
  const reachesTheme = target % 1 === 0 && target !== position;
2960
3517
  if (Math.abs(target - position) >= 25e-4 || reachesTheme) {
2961
3518
  position = target;
2962
3519
  drive(positionPaints, position);
2963
- pixels.paint("position", position);
3520
+ if (holding) artworkWaits ||= mixesArtwork;
3521
+ else pixels.paint("position", position);
2964
3522
  if (plan.sky !== void 0 && typeof map.setSky === "function")
2965
3523
  map.setSky(fillTileflowThemeBlendTemplate(plan.sky, position), { validate: false });
2966
3524
  if (plan.light !== void 0 && typeof map.setLight === "function")
2967
3525
  map.setLight(fillTileflowThemeBlendTemplate(plan.light, position), { validate: false });
3526
+ options.onPosition?.();
3527
+ }
3528
+ if (!hold && holding) {
3529
+ holding = false;
3530
+ if (artworkWaits) pixels.paint("position", position);
3531
+ artworkWaits = false;
2968
3532
  }
2969
3533
  const nextDominant = Math.round(target);
2970
- if (nextDominant === dominant) return;
3534
+ if (nextDominant === dominant || holding) return;
2971
3535
  dominant = nextDominant;
2972
3536
  if (plan.switches.length > 0) {
2973
3537
  const change = () => applySwitches(nextDominant);
@@ -2984,20 +3548,34 @@ function createImageMixer(map, images) {
2984
3548
  const art = /* @__PURE__ */ new Map();
2985
3549
  const shown = /* @__PURE__ */ new Map();
2986
3550
  const unusable = /* @__PURE__ */ new Set();
3551
+ const names = /* @__PURE__ */ new Map();
3552
+ const alike = /* @__PURE__ */ new Map();
2987
3553
  const artOf = (name) => {
2988
3554
  if (!art.has(name)) {
2989
3555
  const image = map.getImage(name)?.data;
2990
3556
  if (!image) return void 0;
2991
- art.set(name, { data: Uint8Array.from(image.data), height: image.height, width: image.width });
3557
+ const copy = { data: Uint8Array.from(image.data), height: image.height, width: image.width };
3558
+ art.set(name, copy);
3559
+ names.set(copy, name);
2992
3560
  }
2993
3561
  return art.get(name);
2994
3562
  };
3563
+ const same2 = (from, to) => {
3564
+ if (from === to) return true;
3565
+ const key = `${names.get(from)}\0${names.get(to)}`;
3566
+ if (!alike.has(key)) {
3567
+ let equal = from.data.length === to.data.length;
3568
+ for (let byte = 0; equal && byte < from.data.length; byte += 1)
3569
+ equal = from.data[byte] === to.data[byte];
3570
+ alike.set(key, equal);
3571
+ }
3572
+ return alike.get(key);
3573
+ };
2995
3574
  return {
2996
3575
  paint(follows, position) {
2997
3576
  const quantized = Math.round(position * 32) / 32;
2998
3577
  for (const image of images) {
2999
- if (image.follows !== follows || unusable.has(image) || shown.get(image) === quantized)
3000
- continue;
3578
+ if (image.follows !== follows || unusable.has(image)) continue;
3001
3579
  const sources = image.names.map(artOf);
3002
3580
  if (sources.some((source) => !source)) continue;
3003
3581
  const [first] = sources;
@@ -3005,14 +3583,21 @@ function createImageMixer(map, images) {
3005
3583
  unusable.add(image);
3006
3584
  continue;
3007
3585
  }
3008
- shown.set(image, quantized);
3586
+ if (!shown.has(image)) shown.set(image, first);
3009
3587
  const index = Math.min(sources.length - 2, Math.floor(quantized));
3010
3588
  const t = quantized - index;
3011
3589
  const from = sources[index];
3012
3590
  const to = sources[index + 1];
3013
- let data = from.data;
3014
- if (t >= 1) data = to.data;
3015
- else if (t > 0 && from !== to) {
3591
+ let content = from;
3592
+ if (t >= 1) content = to;
3593
+ else if (t > 0 && !same2(from, to)) content = `${index}:${t}`;
3594
+ const before = shown.get(image);
3595
+ if (before === content || typeof before !== "string" && typeof content !== "string" && same2(before, content))
3596
+ continue;
3597
+ shown.set(image, content);
3598
+ let data;
3599
+ if (typeof content !== "string") data = content.data;
3600
+ else {
3016
3601
  data = new Uint8Array(from.data.length);
3017
3602
  for (let byte = 0; byte < data.length; byte += 1)
3018
3603
  data[byte] = from.data[byte] + (to.data[byte] - from.data[byte]) * t;
@@ -3022,6 +3607,20 @@ function createImageMixer(map, images) {
3022
3607
  }
3023
3608
  };
3024
3609
  }
3610
+ function relayoutPositions(plan) {
3611
+ const positions = [];
3612
+ const relays = (entry) => entry.group === "layout" || entry.property.endsWith("-pattern") || entry.values.some(readsTileflowFeatureData);
3613
+ for (let index = 0; index + 1 < plan.themes; index += 1) {
3614
+ const switched = plan.switches.some(
3615
+ (entry) => relays(entry) && JSON.stringify(entry.values[index]) !== JSON.stringify(entry.values[index + 1])
3616
+ );
3617
+ const icons = plan.images.some(
3618
+ (image) => image.follows === "dominant" && image.names[index] !== image.names[index + 1]
3619
+ );
3620
+ if (switched || icons) positions.push(index + 0.5);
3621
+ }
3622
+ return positions;
3623
+ }
3025
3624
  function isCrossfadeMap(map) {
3026
3625
  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
3626
  }
@@ -4724,6 +5323,7 @@ l(H, "PMTiles");
4724
5323
  var w = H;
4725
5324
 
4726
5325
  // src/pmtiles-browser.ts
5326
+ var missingRasterTile = null;
4727
5327
  var registeredPmtilesProtocols = /* @__PURE__ */ new WeakSet();
4728
5328
  var maximumSectionBytes = 16 * 1024 * 1024;
4729
5329
  var maximumParserBytes = 32 * 1024 * 1024;
@@ -4751,7 +5351,9 @@ var TileflowPmtilesProtocol = class {
4751
5351
  const archive2 = this.#archive(parseArchiveTarget(request.url));
4752
5352
  const header2 = await archive2.getHeader();
4753
5353
  const tileJson = await archive2.getTileJson(request.url);
4754
- const data = header2.tileType === O.Mlt && isRecord2(tileJson) ? { ...tileJson, encoding: "mlt" } : tileJson;
5354
+ if (!isRecord4(tileJson))
5355
+ throw new TypeError("A PMTiles archive described no TileJSON object.");
5356
+ const data = header2.tileType === O.Mlt ? { ...tileJson, encoding: "mlt" } : tileJson;
4755
5357
  abortController.signal.throwIfAborted();
4756
5358
  return { data };
4757
5359
  }
@@ -4768,7 +5370,7 @@ var TileflowPmtilesProtocol = class {
4768
5370
  };
4769
5371
  }
4770
5372
  return {
4771
- data: header.tileType === O.Mvt || header.tileType === O.Mlt ? new Uint8Array() : null
5373
+ data: header.tileType === O.Mvt || header.tileType === O.Mlt ? new Uint8Array() : missingRasterTile
4772
5374
  };
4773
5375
  };
4774
5376
  tile = protocolCompatibility(this.tilev4);
@@ -4889,7 +5491,7 @@ function normalizeMaximumDirectoryDepth(value) {
4889
5491
  }
4890
5492
  return depth;
4891
5493
  }
4892
- function isRecord2(value) {
5494
+ function isRecord4(value) {
4893
5495
  return value !== null && typeof value === "object" && !Array.isArray(value);
4894
5496
  }
4895
5497
  function protocolCompatibility(handler) {
@@ -4952,10 +5554,22 @@ function handleSystemColorSchemeChange() {
4952
5554
  systemColorScheme = next;
4953
5555
  for (const listener of systemColorSchemeListeners) listener(next);
4954
5556
  }
5557
+ function validateTileflowBrowserThemeBlendSelection(value) {
5558
+ if (!validateTileflowThemeBlendSelection(value)) return false;
5559
+ const { hold } = value;
5560
+ return hold === void 0 || typeof hold === "boolean";
5561
+ }
4955
5562
  var maximumBlendThemes = 8;
4956
5563
  var maximumTransitionMs = 5e3;
4957
5564
  var blendLabelFadeMs = 450;
4958
5565
  var maximumTileflowBlendStyleBytes = 16 * 1024 * 1024;
5566
+ function validateStyleState(name, value) {
5567
+ if (typeof name !== "string" || !name.trim())
5568
+ return new TypeError("Style state names must be non-empty strings.");
5569
+ if (value === null || typeof value === "boolean" || typeof value === "string" || typeof value === "number" && Number.isFinite(value))
5570
+ return void 0;
5571
+ return new TypeError("Style state values must be booleans, finite numbers, strings, or null.");
5572
+ }
4959
5573
  function createTileflowThemeController(options) {
4960
5574
  assertConcreteRuntimeTheme(options.initial, "initial");
4961
5575
  const loadFonts = options.loadFonts ?? ((runtimeStyle) => loadTileflowStyleFonts(runtimeStyle.style, { fontFaces: runtimeStyle.fontFaces }));
@@ -4971,22 +5585,98 @@ function createTileflowThemeController(options) {
4971
5585
  let disposed = false;
4972
5586
  let requestId = 0;
4973
5587
  let applyQueue = Promise.resolve();
5588
+ let shownLayers = typeof options.initial.style === "string" ? void 0 : options.initial.style.layers;
5589
+ let emphasis;
5590
+ let emphasisShown;
5591
+ let emphasisStrength = 0;
5592
+ let emphasisGoal = 0;
5593
+ let emphasisTargets = /* @__PURE__ */ new Map();
5594
+ let emphasisBase = /* @__PURE__ */ new Map();
5595
+ let emphasisBlended = /* @__PURE__ */ new Set();
5596
+ let emphasisGroundTemplate;
5597
+ let emphasisStaticGround;
5598
+ const emphasisWritten = /* @__PURE__ */ new Map();
5599
+ let emphasisRun = 0;
5600
+ let emphasisFrame;
5601
+ let emphasisSettle;
5602
+ const states = /* @__PURE__ */ new Map();
5603
+ let images;
5604
+ function imageContext() {
5605
+ return {
5606
+ theme: current.theme,
5607
+ blend: activeBlend ? Object.freeze({
5608
+ position: activeBlend.blender.position,
5609
+ themes: Object.freeze(activeBlend.themes.map((theme) => theme.theme))
5610
+ }) : void 0,
5611
+ states: Object.freeze(
5612
+ Object.fromEntries([...states].map(([name, state]) => [name, state.shown]))
5613
+ )
5614
+ };
5615
+ }
4974
5616
  return {
4975
5617
  dispose() {
4976
5618
  disposed = true;
5619
+ images?.dispose();
4977
5620
  requestId += 1;
4978
5621
  activeBlend?.blender.dispose();
5622
+ stopEmphasisMotion({ status: "superseded", theme: current.theme });
5623
+ for (const state of states.values())
5624
+ stopStateMotion(state, { status: "superseded", theme: current.theme });
4979
5625
  },
4980
5626
  getBlend() {
4981
5627
  if (!activeBlend) return void 0;
4982
5628
  return {
5629
+ held: activeBlend.blender.held,
4983
5630
  position: activeBlend.blender.position,
5631
+ relayoutAt: activeBlend.blender.relayoutAt,
4984
5632
  themes: activeBlend.themes.map((theme) => theme.theme)
4985
5633
  };
4986
5634
  },
5635
+ registerImage(name, definition) {
5636
+ if (disposed) throw new Error("Tileflow theme controller is disposed.");
5637
+ if (activeBlend?.plan.images.some((image) => image.names.includes(name))) {
5638
+ throw new TypeError("Dynamic image names must remain identical across blended themes.");
5639
+ }
5640
+ images ??= createTileflowDynamicImageRegistry(options.map, imageContext, resetStates);
5641
+ return images.register(name, definition);
5642
+ },
4987
5643
  getCurrent() {
4988
5644
  return current;
4989
5645
  },
5646
+ getEmphasis() {
5647
+ return emphasis;
5648
+ },
5649
+ getState(name) {
5650
+ return typeof name === "string" ? states.get(name.trim())?.value : void 0;
5651
+ },
5652
+ setState(name, value, changeOptions) {
5653
+ const invalid = validateStyleState(name, value);
5654
+ if (invalid) return Promise.resolve({ error: invalid, status: "failed", theme: current.theme });
5655
+ if (disposed) return Promise.resolve(disposedResult(current.theme));
5656
+ if (typeof options.map.setGlobalStateProperty !== "function") {
5657
+ return Promise.resolve({
5658
+ error: new TypeError("Style states need a MapLibre map with global state (GL JS 5.6+)."),
5659
+ status: "failed",
5660
+ theme: current.theme
5661
+ });
5662
+ }
5663
+ const transition = transitionOf(changeOptions);
5664
+ if (transition instanceof TypeError)
5665
+ return Promise.resolve({ error: transition, status: "failed", theme: current.theme });
5666
+ return changeState(name.trim(), value, reducedMotion() ? 0 : transition);
5667
+ },
5668
+ setEmphasis(request, changeOptions) {
5669
+ if (request !== void 0) {
5670
+ const invalid = validateTileflowEmphasis(request);
5671
+ if (invalid)
5672
+ return Promise.resolve({ error: invalid, status: "failed", theme: current.theme });
5673
+ }
5674
+ if (disposed) return Promise.resolve(disposedResult(current.theme));
5675
+ const transition = transitionOf(changeOptions);
5676
+ if (transition instanceof TypeError)
5677
+ return Promise.resolve({ error: transition, status: "failed", theme: current.theme });
5678
+ return changeEmphasis(request, reducedMotion() ? 0 : transition);
5679
+ },
4990
5680
  setBlend(request, changeOptions) {
4991
5681
  const targetTheme = nearestTheme(request);
4992
5682
  const invalid = validateBlendRequest(request);
@@ -4998,8 +5688,9 @@ function createTileflowThemeController(options) {
4998
5688
  const key = blendKey(request.themes);
4999
5689
  if (activeBlend?.key === key) {
5000
5690
  requestId += 1;
5001
- activeBlend.blender.set(request.position);
5691
+ activeBlend.blender.set(request.position, { hold: request.hold === true });
5002
5692
  current = activeBlend.themes[activeBlend.blender.dominant];
5693
+ images?.invalidate();
5003
5694
  return Promise.resolve({ status: "applied", theme: current.theme });
5004
5695
  }
5005
5696
  if (pendingBlend?.key === key) {
@@ -5087,7 +5778,9 @@ function createTileflowThemeController(options) {
5087
5778
  ),
5088
5779
  Promise.all(themes.map((theme) => loadFonts(theme)))
5089
5780
  ]);
5090
- return planTileflowThemeBlend(styles);
5781
+ const plan = planTileflowThemeBlend(styles);
5782
+ images?.assertBlend(plan.images);
5783
+ return plan;
5091
5784
  }
5092
5785
  function enqueue(runId, target) {
5093
5786
  const operation = applyQueue.then(
@@ -5106,6 +5799,8 @@ function createTileflowThemeController(options) {
5106
5799
  async function applyTarget(runId, target) {
5107
5800
  const targetTheme = targetThemeOf(target);
5108
5801
  if (disposed || runId !== requestId) return { status: "superseded", theme: targetTheme };
5802
+ await whenContextAvailable(options.map);
5803
+ if (disposed || runId !== requestId) return { status: "superseded", theme: targetTheme };
5109
5804
  const previous = { current, shown };
5110
5805
  options.onTransition?.({ currentTheme: previous.current.theme, phase: "applying", targetTheme });
5111
5806
  const cover = beginTileflowStyleCrossfade(options.map, target.transition);
@@ -5113,6 +5808,7 @@ function createTileflowThemeController(options) {
5113
5808
  activeBlend = void 0;
5114
5809
  const styleInput = target.kind === "theme" ? target.style.style : target.plan.styleAt(target.position());
5115
5810
  try {
5811
+ if (target.kind === "blend") images?.assertBlend(target.plan.images);
5116
5812
  await applyMapStyle(options.map, styleInput, timeoutMs);
5117
5813
  if (disposed) {
5118
5814
  cover?.cancel();
@@ -5136,6 +5832,10 @@ function createTileflowThemeController(options) {
5136
5832
  current = target.themes[blender.dominant];
5137
5833
  if (pendingBlend?.key === target.key) pendingBlend = void 0;
5138
5834
  }
5835
+ shownLayers = typeof styleInput === "string" ? void 0 : styleInput.layers;
5836
+ resetEmphasis();
5837
+ resetStates();
5838
+ images?.invalidate();
5139
5839
  await cover?.finish({ stop: () => disposed || runId !== requestId });
5140
5840
  options.onTransition?.({
5141
5841
  currentTheme: current.theme,
@@ -5168,10 +5868,16 @@ function createTileflowThemeController(options) {
5168
5868
  void fade?.finish();
5169
5869
  },
5170
5870
  labelFadeMs: reducedMotion() ? 0 : blendLabelFadeMs,
5171
- position
5871
+ // The ground colour moves with the blend, and colours that recede follow it.
5872
+ onPosition: () => {
5873
+ if (emphasisShown) writeEmphasis(false);
5874
+ },
5875
+ position,
5876
+ transform: (layer, property, value) => emphasisValue(layer, property, value)
5172
5877
  });
5173
5878
  }
5174
5879
  async function restore(previous) {
5880
+ await whenContextAvailable(options.map);
5175
5881
  const blend = previous.shown.blend;
5176
5882
  if (blend) {
5177
5883
  const position = blend.blender.position;
@@ -5179,11 +5885,260 @@ function createTileflowThemeController(options) {
5179
5885
  await applyMapStyle(options.map, style, timeoutMs);
5180
5886
  activeBlend = { ...blend, blender: createBlender(blend.plan, position) };
5181
5887
  shown = { blend: activeBlend, style };
5888
+ shownLayers = style.layers;
5182
5889
  } else {
5183
5890
  await applyMapStyle(options.map, previous.shown.style, timeoutMs);
5184
5891
  shown = previous.shown;
5892
+ shownLayers = typeof shown.style === "string" ? void 0 : shown.style.layers;
5185
5893
  }
5186
5894
  current = previous.current;
5895
+ resetEmphasis();
5896
+ resetStates();
5897
+ images?.invalidate();
5898
+ }
5899
+ function writeState(name, value) {
5900
+ const set = options.map.setGlobalStateProperty;
5901
+ try {
5902
+ set?.call(options.map, name, value);
5903
+ } catch {
5904
+ }
5905
+ images?.invalidate(name);
5906
+ }
5907
+ function resetStates() {
5908
+ for (const [name, state] of states) {
5909
+ if (state.shown !== void 0) writeState(name, state.shown);
5910
+ }
5911
+ }
5912
+ function stopStateMotion(state, result) {
5913
+ state.stop?.();
5914
+ state.stop = void 0;
5915
+ const settle = state.settle;
5916
+ state.settle = void 0;
5917
+ if (result) settle?.(result);
5918
+ }
5919
+ function changeState(name, value, duration) {
5920
+ const state = states.get(name) ?? { run: 0, shown: void 0, value };
5921
+ states.set(name, state);
5922
+ stopStateMotion(state, { status: "superseded", theme: current.theme });
5923
+ const run = ++state.run;
5924
+ const from = state.shown;
5925
+ state.value = value;
5926
+ const view = tileflowThemeMotionView(options.map);
5927
+ if (duration <= 0 || !view || typeof from !== "number" || typeof value !== "number" || from === value) {
5928
+ state.shown = value;
5929
+ writeState(name, value);
5930
+ return Promise.resolve({ status: "applied", theme: current.theme });
5931
+ }
5932
+ return new Promise((resolve) => {
5933
+ state.settle = resolve;
5934
+ const now = () => view.performance?.now?.() ?? Date.now();
5935
+ const start = now();
5936
+ const least = Math.abs(value - from) / 100;
5937
+ let cancelled = false;
5938
+ let frame;
5939
+ const step = () => {
5940
+ frame = void 0;
5941
+ if (cancelled || disposed || run !== state.run) return;
5942
+ const t = Math.min(1, (now() - start) / duration);
5943
+ const eased = t * t * (3 - 2 * t);
5944
+ const shown2 = t >= 1 ? value : from + (value - from) * eased;
5945
+ if (t >= 1 || Math.abs(shown2 - state.shown) >= least) {
5946
+ state.shown = shown2;
5947
+ writeState(name, shown2);
5948
+ }
5949
+ if (t >= 1) {
5950
+ state.stop = void 0;
5951
+ state.settle = void 0;
5952
+ resolve({ status: "applied", theme: current.theme });
5953
+ return;
5954
+ }
5955
+ frame = view.requestAnimationFrame(step);
5956
+ };
5957
+ state.stop = () => {
5958
+ cancelled = true;
5959
+ if (frame !== void 0) view.cancelAnimationFrame(frame);
5960
+ };
5961
+ frame = view.requestAnimationFrame(step);
5962
+ });
5963
+ }
5964
+ function valueKey(layer, property) {
5965
+ return `${layer}\0${property}`;
5966
+ }
5967
+ function layersShown() {
5968
+ if (shownLayers) return shownLayers;
5969
+ const getStyle = options.map.getStyle;
5970
+ if (typeof getStyle !== "function") return [];
5971
+ const style = getStyle.call(options.map);
5972
+ shownLayers = Array.isArray(style?.layers) ? style.layers : [];
5973
+ return shownLayers;
5974
+ }
5975
+ function planEmphasis(onMap = /* @__PURE__ */ new Map()) {
5976
+ const layers = layersShown();
5977
+ const targets = emphasisShown ? planTileflowEmphasis(layers, emphasisShown) : [];
5978
+ emphasisTargets = new Map(
5979
+ targets.map((target) => [valueKey(target.layer, target.property), target])
5980
+ );
5981
+ const paintOf = /* @__PURE__ */ new Map();
5982
+ for (const layer of layers) {
5983
+ if (layer && typeof layer === "object" && typeof layer.id === "string") {
5984
+ const paint = layer.paint;
5985
+ paintOf.set(
5986
+ layer.id,
5987
+ paint && typeof paint === "object" ? paint : {}
5988
+ );
5989
+ }
5990
+ }
5991
+ emphasisBase = new Map(
5992
+ targets.map((target) => [
5993
+ valueKey(target.layer, target.property),
5994
+ paintOf.get(target.layer)?.[target.property]
5995
+ ])
5996
+ );
5997
+ emphasisStaticGround = tileflowEmphasisGround(layers);
5998
+ emphasisBlended = new Set(
5999
+ activeBlend?.plan.paints.map((paint) => valueKey(paint.layer, paint.property)) ?? []
6000
+ );
6001
+ const background = layers.find(
6002
+ (layer) => layer && typeof layer === "object" && layer.type === "background"
6003
+ );
6004
+ emphasisGroundTemplate = activeBlend?.plan.paints.find(
6005
+ (paint) => paint.layer === background?.id && paint.property === "background-color"
6006
+ )?.template;
6007
+ emphasisWritten.clear();
6008
+ for (const [key, base] of emphasisBase) {
6009
+ emphasisWritten.set(key, onMap.get(key) ?? JSON.stringify(base ?? null));
6010
+ }
6011
+ }
6012
+ function resetEmphasis() {
6013
+ if (!emphasisShown) return;
6014
+ planEmphasis();
6015
+ writeEmphasis(true);
6016
+ }
6017
+ function emphasisGround() {
6018
+ if (activeBlend && emphasisGroundTemplate !== void 0) {
6019
+ return firstColour(
6020
+ fillTileflowThemeBlendTemplate(emphasisGroundTemplate, activeBlend.blender.position)
6021
+ );
6022
+ }
6023
+ return emphasisStaticGround;
6024
+ }
6025
+ function emphasisValue(layer, property, value) {
6026
+ const target = emphasisShown ? emphasisTargets.get(valueKey(layer, property)) : void 0;
6027
+ if (!target) return value;
6028
+ const strength = target.featureData ? emphasisGoal : emphasisStrength;
6029
+ return emphasizeTileflowValue(target, value, {
6030
+ ground: emphasisGround(),
6031
+ keep: strength > 0 ? emphasisShown?.keep : void 0,
6032
+ strength
6033
+ });
6034
+ }
6035
+ function writeEmphasis(blended) {
6036
+ if (blended && activeBlend && emphasisTargets.size > 0) {
6037
+ activeBlend.blender.refresh(
6038
+ (layer, property) => emphasisTargets.has(valueKey(layer, property))
6039
+ );
6040
+ }
6041
+ const setPaint = options.map.setPaintProperty;
6042
+ const getLayer = options.map.getLayer;
6043
+ if (typeof setPaint !== "function") return;
6044
+ for (const [key, target] of emphasisTargets) {
6045
+ if (emphasisBlended.has(key)) continue;
6046
+ const value = emphasisValue(target.layer, target.property, emphasisBase.get(key));
6047
+ const serialized = JSON.stringify(value ?? null);
6048
+ if (emphasisWritten.get(key) === serialized) continue;
6049
+ if (typeof getLayer === "function" && !getLayer.call(options.map, target.layer)) continue;
6050
+ emphasisWritten.set(key, serialized);
6051
+ setPaint.call(options.map, target.layer, target.property, value, { validate: false });
6052
+ }
6053
+ }
6054
+ function stopEmphasisMotion(result) {
6055
+ emphasisFrame?.();
6056
+ emphasisFrame = void 0;
6057
+ const settle = emphasisSettle;
6058
+ emphasisSettle = void 0;
6059
+ if (result) settle?.(result);
6060
+ }
6061
+ function changeEmphasis(request, duration) {
6062
+ stopEmphasisMotion({ status: "superseded", theme: current.theme });
6063
+ const run = ++emphasisRun;
6064
+ emphasis = request;
6065
+ const before = emphasisTargets;
6066
+ if (request) {
6067
+ const leaving = new Map(emphasisWritten);
6068
+ const previous = emphasisShown;
6069
+ emphasisShown = request;
6070
+ planEmphasis(leaving);
6071
+ if (previous) {
6072
+ const setPaint = options.map.setPaintProperty;
6073
+ for (const [key, target] of before) {
6074
+ if (emphasisTargets.has(key) || typeof setPaint !== "function") continue;
6075
+ const base = layerPaint(target.layer, target.property);
6076
+ setPaint.call(options.map, target.layer, target.property, base, { validate: false });
6077
+ }
6078
+ }
6079
+ }
6080
+ if (!emphasisShown) return Promise.resolve({ status: "applied", theme: current.theme });
6081
+ emphasisGoal = request ? 1 : 0;
6082
+ const from = emphasisStrength;
6083
+ const to = emphasisGoal;
6084
+ const switches = [...emphasisTargets.values()].some((target) => target.featureData);
6085
+ const cover = switches ? beginTileflowStyleCrossfade(options.map, duration) : void 0;
6086
+ const view = tileflowThemeMotionView(options.map);
6087
+ return new Promise((resolve) => {
6088
+ emphasisSettle = resolve;
6089
+ const finish = () => {
6090
+ emphasisFrame = void 0;
6091
+ emphasisStrength = to;
6092
+ writeEmphasis(true);
6093
+ if (to === 0) {
6094
+ emphasisShown = void 0;
6095
+ emphasisTargets = /* @__PURE__ */ new Map();
6096
+ emphasisWritten.clear();
6097
+ }
6098
+ emphasisSettle = void 0;
6099
+ resolve({ status: "applied", theme: current.theme });
6100
+ };
6101
+ writeEmphasis(true);
6102
+ void cover?.finish({ stop: () => disposed || run !== emphasisRun });
6103
+ if (duration <= 0 || !view || from === to) {
6104
+ finish();
6105
+ return;
6106
+ }
6107
+ const now = () => view.performance?.now?.() ?? Date.now();
6108
+ const start = now();
6109
+ let cancelled = false;
6110
+ let frame;
6111
+ const step = () => {
6112
+ frame = void 0;
6113
+ if (cancelled || disposed || run !== emphasisRun) return;
6114
+ const t = Math.min(1, (now() - start) / duration);
6115
+ const eased = t * t * (3 - 2 * t);
6116
+ const strength = from + (to - from) * eased;
6117
+ if (t >= 1) return finish();
6118
+ if (Math.abs(strength - emphasisStrength) >= 0.01) {
6119
+ emphasisStrength = strength;
6120
+ writeEmphasis(true);
6121
+ }
6122
+ frame = view.requestAnimationFrame(step);
6123
+ };
6124
+ emphasisFrame = () => {
6125
+ cancelled = true;
6126
+ if (frame !== void 0) view.cancelAnimationFrame(frame);
6127
+ };
6128
+ frame = view.requestAnimationFrame(step);
6129
+ });
6130
+ }
6131
+ function layerPaint(layer, property) {
6132
+ const key = valueKey(layer, property);
6133
+ if (activeBlend && emphasisBlended.has(key))
6134
+ return activeBlend.blender.baseValue(layer, property);
6135
+ for (const entry of layersShown()) {
6136
+ if (entry && typeof entry === "object" && entry.id === layer) {
6137
+ const paint = entry.paint;
6138
+ return paint?.[property];
6139
+ }
6140
+ }
6141
+ return void 0;
5187
6142
  }
5188
6143
  function reducedMotion() {
5189
6144
  const view = tileflowThemeMotionView(options.map);
@@ -5265,6 +6220,26 @@ function validateConcreteRuntimeTheme(style, role) {
5265
6220
  `Tileflow theme controller ${role} style requires a concrete portable theme name; received ${JSON.stringify(style.theme)}.`
5266
6221
  );
5267
6222
  }
6223
+ function whenContextAvailable(map) {
6224
+ if (!isContextLost(map)) return Promise.resolve();
6225
+ return new Promise((resolve) => {
6226
+ const restored = () => {
6227
+ map.off("webglcontextrestored", restored);
6228
+ resolve();
6229
+ };
6230
+ map.on("webglcontextrestored", restored);
6231
+ });
6232
+ }
6233
+ function isContextLost(map) {
6234
+ if (typeof map.getCanvas !== "function") return false;
6235
+ try {
6236
+ const canvas = map.getCanvas.call(map);
6237
+ const context = canvas?.getContext?.("webgl2") ?? canvas?.getContext?.("webgl");
6238
+ return context?.isContextLost?.() === true;
6239
+ } catch {
6240
+ return false;
6241
+ }
6242
+ }
5268
6243
  function applyMapStyle(map, style, timeoutMs) {
5269
6244
  return new Promise((resolve, reject) => {
5270
6245
  let settled = false;
@@ -5574,37 +6549,21 @@ function attachTileflowMapLifecycle(options) {
5574
6549
  };
5575
6550
  }
5576
6551
  function createTileflowTransformRequest(options) {
5577
- if (!options.always && !options.transformRequest && !options.worldRequestBridge) {
5578
- const analytics = options.getAnalytics();
5579
- const requiresCommercialSession = Boolean(options.sessionController && analytics?.mapId);
5580
- if (!requiresCommercialSession && (!analytics || analytics.enabled === false)) {
5581
- return void 0;
5582
- }
5583
- }
5584
- const asyncAnalyticsTiming = options.asyncAnalyticsTiming ?? "request";
5585
6552
  return (url, resourceType) => {
5586
6553
  const request = options.transformRequest?.(url, resourceType);
5587
- const analyticsAtRequest = asyncAnalyticsTiming === "request" ? options.getAnalytics() : void 0;
5588
- if (isPromiseLike(request)) {
5589
- return request.then(
5590
- (resolvedRequest) => applyTileflowRequest(
5591
- url,
5592
- resolvedRequest,
5593
- asyncAnalyticsTiming === "resolution" ? options.getAnalytics() : analyticsAtRequest,
5594
- options.sessionId,
5595
- options.sessionController,
5596
- options.worldRequestBridge
5597
- ) ?? resolvedRequest
5598
- );
5599
- }
5600
- return applyTileflowRequest(
6554
+ const analytics = options.getAnalytics();
6555
+ const apply = (resolvedRequest) => applyTileflowRequest(
5601
6556
  url,
5602
- request,
5603
- asyncAnalyticsTiming === "resolution" ? options.getAnalytics() : analyticsAtRequest,
6557
+ resolvedRequest,
6558
+ analytics,
5604
6559
  options.sessionId,
5605
6560
  options.sessionController,
5606
6561
  options.worldRequestBridge
5607
6562
  );
6563
+ if (isPromiseLike(request)) {
6564
+ return request.then((resolvedRequest) => apply(resolvedRequest) ?? resolvedRequest);
6565
+ }
6566
+ return apply(request);
5608
6567
  };
5609
6568
  }
5610
6569
  function applyTileflowRequest(url, request, analytics, sessionId, sessionController, worldRequestBridge) {
@@ -5670,9 +6629,12 @@ export {
5670
6629
  registerTileflowWorldRequestBridge,
5671
6630
  subscribeTileflowSystemColorScheme,
5672
6631
  tileflowFairUseNoticeMessage,
6632
+ tileflowLayerDomainMetadataKey,
5673
6633
  tileflowMaplibreContourVersion,
5674
6634
  tileflowPmtilesMaximumDirectoryDepth,
5675
- tileflowPmtilesProtocol
6635
+ tileflowPmtilesProtocol,
6636
+ validateTileflowBrowserThemeBlendSelection,
6637
+ validateTileflowEmphasis
5676
6638
  };
5677
6639
  /*! Bundled license information:
5678
6640