@tileflow/core 0.1.0-alpha.31 → 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 [];
@@ -3181,6 +3407,8 @@ function createTileflowThemeBlender(input, plan, options) {
3181
3407
  const positionPaints = plan.paints.filter((paint) => paint.follows === "position");
3182
3408
  const dominantPaints = plan.paints.filter((paint) => paint.follows === "dominant");
3183
3409
  const pixels = createImageMixer(map, plan.images);
3410
+ const mixesArtwork = plan.images.some((image) => image.follows === "position");
3411
+ const relayoutAt = relayoutPositions(plan);
3184
3412
  const shownValues = /* @__PURE__ */ new Map();
3185
3413
  const baseValues = /* @__PURE__ */ new Map();
3186
3414
  const valueKey = (layer, property) => `${layer}\0${property}`;
@@ -3191,6 +3419,8 @@ function createTileflowThemeBlender(input, plan, options) {
3191
3419
  let fade;
3192
3420
  let frameRequest;
3193
3421
  let disposed = false;
3422
+ let holding = false;
3423
+ let artworkWaits = false;
3194
3424
  pixels.paint("position", position);
3195
3425
  pixels.paint("dominant", label);
3196
3426
  const drive = (paints, at) => {
@@ -3241,9 +3471,13 @@ function createTileflowThemeBlender(input, plan, options) {
3241
3471
  get dominant() {
3242
3472
  return dominant;
3243
3473
  },
3474
+ get held() {
3475
+ return holding && (artworkWaits || Math.round(position) !== dominant);
3476
+ },
3244
3477
  get position() {
3245
3478
  return position;
3246
3479
  },
3480
+ relayoutAt,
3247
3481
  refresh(include) {
3248
3482
  if (disposed) return;
3249
3483
  drive(
@@ -3274,22 +3508,30 @@ function createTileflowThemeBlender(input, plan, options) {
3274
3508
  }
3275
3509
  frameRequest = void 0;
3276
3510
  },
3277
- set(next) {
3511
+ set(next, setOptions) {
3278
3512
  if (disposed) return;
3513
+ const hold = setOptions?.hold === true;
3514
+ if (hold) holding = true;
3279
3515
  const target = clamp(next);
3280
3516
  const reachesTheme = target % 1 === 0 && target !== position;
3281
3517
  if (Math.abs(target - position) >= 25e-4 || reachesTheme) {
3282
3518
  position = target;
3283
3519
  drive(positionPaints, position);
3284
- pixels.paint("position", position);
3520
+ if (holding) artworkWaits ||= mixesArtwork;
3521
+ else pixels.paint("position", position);
3285
3522
  if (plan.sky !== void 0 && typeof map.setSky === "function")
3286
3523
  map.setSky(fillTileflowThemeBlendTemplate(plan.sky, position), { validate: false });
3287
3524
  if (plan.light !== void 0 && typeof map.setLight === "function")
3288
3525
  map.setLight(fillTileflowThemeBlendTemplate(plan.light, position), { validate: false });
3289
3526
  options.onPosition?.();
3290
3527
  }
3528
+ if (!hold && holding) {
3529
+ holding = false;
3530
+ if (artworkWaits) pixels.paint("position", position);
3531
+ artworkWaits = false;
3532
+ }
3291
3533
  const nextDominant = Math.round(target);
3292
- if (nextDominant === dominant) return;
3534
+ if (nextDominant === dominant || holding) return;
3293
3535
  dominant = nextDominant;
3294
3536
  if (plan.switches.length > 0) {
3295
3537
  const change = () => applySwitches(nextDominant);
@@ -3306,20 +3548,34 @@ function createImageMixer(map, images) {
3306
3548
  const art = /* @__PURE__ */ new Map();
3307
3549
  const shown = /* @__PURE__ */ new Map();
3308
3550
  const unusable = /* @__PURE__ */ new Set();
3551
+ const names = /* @__PURE__ */ new Map();
3552
+ const alike = /* @__PURE__ */ new Map();
3309
3553
  const artOf = (name) => {
3310
3554
  if (!art.has(name)) {
3311
3555
  const image = map.getImage(name)?.data;
3312
3556
  if (!image) return void 0;
3313
- 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);
3314
3560
  }
3315
3561
  return art.get(name);
3316
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
+ };
3317
3574
  return {
3318
3575
  paint(follows, position) {
3319
3576
  const quantized = Math.round(position * 32) / 32;
3320
3577
  for (const image of images) {
3321
- if (image.follows !== follows || unusable.has(image) || shown.get(image) === quantized)
3322
- continue;
3578
+ if (image.follows !== follows || unusable.has(image)) continue;
3323
3579
  const sources = image.names.map(artOf);
3324
3580
  if (sources.some((source) => !source)) continue;
3325
3581
  const [first] = sources;
@@ -3327,14 +3583,21 @@ function createImageMixer(map, images) {
3327
3583
  unusable.add(image);
3328
3584
  continue;
3329
3585
  }
3330
- shown.set(image, quantized);
3586
+ if (!shown.has(image)) shown.set(image, first);
3331
3587
  const index = Math.min(sources.length - 2, Math.floor(quantized));
3332
3588
  const t = quantized - index;
3333
3589
  const from = sources[index];
3334
3590
  const to = sources[index + 1];
3335
- let data = from.data;
3336
- if (t >= 1) data = to.data;
3337
- 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 {
3338
3601
  data = new Uint8Array(from.data.length);
3339
3602
  for (let byte = 0; byte < data.length; byte += 1)
3340
3603
  data[byte] = from.data[byte] + (to.data[byte] - from.data[byte]) * t;
@@ -3344,6 +3607,20 @@ function createImageMixer(map, images) {
3344
3607
  }
3345
3608
  };
3346
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
+ }
3347
3624
  function isCrossfadeMap(map) {
3348
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";
3349
3626
  }
@@ -5046,6 +5323,7 @@ l(H, "PMTiles");
5046
5323
  var w = H;
5047
5324
 
5048
5325
  // src/pmtiles-browser.ts
5326
+ var missingRasterTile = null;
5049
5327
  var registeredPmtilesProtocols = /* @__PURE__ */ new WeakSet();
5050
5328
  var maximumSectionBytes = 16 * 1024 * 1024;
5051
5329
  var maximumParserBytes = 32 * 1024 * 1024;
@@ -5073,7 +5351,9 @@ var TileflowPmtilesProtocol = class {
5073
5351
  const archive2 = this.#archive(parseArchiveTarget(request.url));
5074
5352
  const header2 = await archive2.getHeader();
5075
5353
  const tileJson = await archive2.getTileJson(request.url);
5076
- const data = header2.tileType === O.Mlt && isRecord4(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;
5077
5357
  abortController.signal.throwIfAborted();
5078
5358
  return { data };
5079
5359
  }
@@ -5090,7 +5370,7 @@ var TileflowPmtilesProtocol = class {
5090
5370
  };
5091
5371
  }
5092
5372
  return {
5093
- 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
5094
5374
  };
5095
5375
  };
5096
5376
  tile = protocolCompatibility(this.tilev4);
@@ -5274,10 +5554,22 @@ function handleSystemColorSchemeChange() {
5274
5554
  systemColorScheme = next;
5275
5555
  for (const listener of systemColorSchemeListeners) listener(next);
5276
5556
  }
5557
+ function validateTileflowBrowserThemeBlendSelection(value) {
5558
+ if (!validateTileflowThemeBlendSelection(value)) return false;
5559
+ const { hold } = value;
5560
+ return hold === void 0 || typeof hold === "boolean";
5561
+ }
5277
5562
  var maximumBlendThemes = 8;
5278
5563
  var maximumTransitionMs = 5e3;
5279
5564
  var blendLabelFadeMs = 450;
5280
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
+ }
5281
5573
  function createTileflowThemeController(options) {
5282
5574
  assertConcreteRuntimeTheme(options.initial, "initial");
5283
5575
  const loadFonts = options.loadFonts ?? ((runtimeStyle) => loadTileflowStyleFonts(runtimeStyle.style, { fontFaces: runtimeStyle.fontFaces }));
@@ -5307,26 +5599,72 @@ function createTileflowThemeController(options) {
5307
5599
  let emphasisRun = 0;
5308
5600
  let emphasisFrame;
5309
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
+ }
5310
5616
  return {
5311
5617
  dispose() {
5312
5618
  disposed = true;
5619
+ images?.dispose();
5313
5620
  requestId += 1;
5314
5621
  activeBlend?.blender.dispose();
5315
5622
  stopEmphasisMotion({ status: "superseded", theme: current.theme });
5623
+ for (const state of states.values())
5624
+ stopStateMotion(state, { status: "superseded", theme: current.theme });
5316
5625
  },
5317
5626
  getBlend() {
5318
5627
  if (!activeBlend) return void 0;
5319
5628
  return {
5629
+ held: activeBlend.blender.held,
5320
5630
  position: activeBlend.blender.position,
5631
+ relayoutAt: activeBlend.blender.relayoutAt,
5321
5632
  themes: activeBlend.themes.map((theme) => theme.theme)
5322
5633
  };
5323
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
+ },
5324
5643
  getCurrent() {
5325
5644
  return current;
5326
5645
  },
5327
5646
  getEmphasis() {
5328
5647
  return emphasis;
5329
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
+ },
5330
5668
  setEmphasis(request, changeOptions) {
5331
5669
  if (request !== void 0) {
5332
5670
  const invalid = validateTileflowEmphasis(request);
@@ -5350,8 +5688,9 @@ function createTileflowThemeController(options) {
5350
5688
  const key = blendKey(request.themes);
5351
5689
  if (activeBlend?.key === key) {
5352
5690
  requestId += 1;
5353
- activeBlend.blender.set(request.position);
5691
+ activeBlend.blender.set(request.position, { hold: request.hold === true });
5354
5692
  current = activeBlend.themes[activeBlend.blender.dominant];
5693
+ images?.invalidate();
5355
5694
  return Promise.resolve({ status: "applied", theme: current.theme });
5356
5695
  }
5357
5696
  if (pendingBlend?.key === key) {
@@ -5439,7 +5778,9 @@ function createTileflowThemeController(options) {
5439
5778
  ),
5440
5779
  Promise.all(themes.map((theme) => loadFonts(theme)))
5441
5780
  ]);
5442
- return planTileflowThemeBlend(styles);
5781
+ const plan = planTileflowThemeBlend(styles);
5782
+ images?.assertBlend(plan.images);
5783
+ return plan;
5443
5784
  }
5444
5785
  function enqueue(runId, target) {
5445
5786
  const operation = applyQueue.then(
@@ -5458,6 +5799,8 @@ function createTileflowThemeController(options) {
5458
5799
  async function applyTarget(runId, target) {
5459
5800
  const targetTheme = targetThemeOf(target);
5460
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 };
5461
5804
  const previous = { current, shown };
5462
5805
  options.onTransition?.({ currentTheme: previous.current.theme, phase: "applying", targetTheme });
5463
5806
  const cover = beginTileflowStyleCrossfade(options.map, target.transition);
@@ -5465,6 +5808,7 @@ function createTileflowThemeController(options) {
5465
5808
  activeBlend = void 0;
5466
5809
  const styleInput = target.kind === "theme" ? target.style.style : target.plan.styleAt(target.position());
5467
5810
  try {
5811
+ if (target.kind === "blend") images?.assertBlend(target.plan.images);
5468
5812
  await applyMapStyle(options.map, styleInput, timeoutMs);
5469
5813
  if (disposed) {
5470
5814
  cover?.cancel();
@@ -5490,6 +5834,8 @@ function createTileflowThemeController(options) {
5490
5834
  }
5491
5835
  shownLayers = typeof styleInput === "string" ? void 0 : styleInput.layers;
5492
5836
  resetEmphasis();
5837
+ resetStates();
5838
+ images?.invalidate();
5493
5839
  await cover?.finish({ stop: () => disposed || runId !== requestId });
5494
5840
  options.onTransition?.({
5495
5841
  currentTheme: current.theme,
@@ -5531,6 +5877,7 @@ function createTileflowThemeController(options) {
5531
5877
  });
5532
5878
  }
5533
5879
  async function restore(previous) {
5880
+ await whenContextAvailable(options.map);
5534
5881
  const blend = previous.shown.blend;
5535
5882
  if (blend) {
5536
5883
  const position = blend.blender.position;
@@ -5546,6 +5893,73 @@ function createTileflowThemeController(options) {
5546
5893
  }
5547
5894
  current = previous.current;
5548
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
+ });
5549
5963
  }
5550
5964
  function valueKey(layer, property) {
5551
5965
  return `${layer}\0${property}`;
@@ -5806,6 +6220,26 @@ function validateConcreteRuntimeTheme(style, role) {
5806
6220
  `Tileflow theme controller ${role} style requires a concrete portable theme name; received ${JSON.stringify(style.theme)}.`
5807
6221
  );
5808
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
+ }
5809
6243
  function applyMapStyle(map, style, timeoutMs) {
5810
6244
  return new Promise((resolve, reject) => {
5811
6245
  let settled = false;
@@ -6115,37 +6549,21 @@ function attachTileflowMapLifecycle(options) {
6115
6549
  };
6116
6550
  }
6117
6551
  function createTileflowTransformRequest(options) {
6118
- if (!options.always && !options.transformRequest && !options.worldRequestBridge) {
6119
- const analytics = options.getAnalytics();
6120
- const requiresCommercialSession = Boolean(options.sessionController && analytics?.mapId);
6121
- if (!requiresCommercialSession && (!analytics || analytics.enabled === false)) {
6122
- return void 0;
6123
- }
6124
- }
6125
- const asyncAnalyticsTiming = options.asyncAnalyticsTiming ?? "request";
6126
6552
  return (url, resourceType) => {
6127
6553
  const request = options.transformRequest?.(url, resourceType);
6128
- const analyticsAtRequest = asyncAnalyticsTiming === "request" ? options.getAnalytics() : void 0;
6129
- if (isPromiseLike(request)) {
6130
- return request.then(
6131
- (resolvedRequest) => applyTileflowRequest(
6132
- url,
6133
- resolvedRequest,
6134
- asyncAnalyticsTiming === "resolution" ? options.getAnalytics() : analyticsAtRequest,
6135
- options.sessionId,
6136
- options.sessionController,
6137
- options.worldRequestBridge
6138
- ) ?? resolvedRequest
6139
- );
6140
- }
6141
- return applyTileflowRequest(
6554
+ const analytics = options.getAnalytics();
6555
+ const apply = (resolvedRequest) => applyTileflowRequest(
6142
6556
  url,
6143
- request,
6144
- asyncAnalyticsTiming === "resolution" ? options.getAnalytics() : analyticsAtRequest,
6557
+ resolvedRequest,
6558
+ analytics,
6145
6559
  options.sessionId,
6146
6560
  options.sessionController,
6147
6561
  options.worldRequestBridge
6148
6562
  );
6563
+ if (isPromiseLike(request)) {
6564
+ return request.then((resolvedRequest) => apply(resolvedRequest) ?? resolvedRequest);
6565
+ }
6566
+ return apply(request);
6149
6567
  };
6150
6568
  }
6151
6569
  function applyTileflowRequest(url, request, analytics, sessionId, sessionController, worldRequestBridge) {
@@ -6215,6 +6633,7 @@ export {
6215
6633
  tileflowMaplibreContourVersion,
6216
6634
  tileflowPmtilesMaximumDirectoryDepth,
6217
6635
  tileflowPmtilesProtocol,
6636
+ validateTileflowBrowserThemeBlendSelection,
6218
6637
  validateTileflowEmphasis
6219
6638
  };
6220
6639
  /*! Bundled license information: