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

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
@@ -2238,17 +2238,21 @@ var featureOperators = /* @__PURE__ */ new Set([
2238
2238
  "properties"
2239
2239
  ]);
2240
2240
  var maximumBranches = 24;
2241
- function planTileflowThemeBlend(styles) {
2241
+ function planTileflowThemeBlend(styles, options = {}) {
2242
2242
  if (styles.length < 2)
2243
2243
  throw new TileflowThemeBlendError("A theme blend needs two or more themes.");
2244
2244
  const [base] = styles;
2245
2245
  assertSameStructure(styles, base);
2246
+ const mixedImages = new Set(imageProperties);
2247
+ if (options.iconImages === "switch") mixedImages.delete("icon-image");
2246
2248
  const paints = [];
2247
2249
  const switches = [];
2248
- const imageGroups = collectImageNames(styles, base);
2250
+ const imageGroups = collectImageNames(styles, base, mixedImages);
2251
+ const symbolLayers = /* @__PURE__ */ new Set();
2249
2252
  const images = /* @__PURE__ */ new Map();
2250
2253
  let splitLayers = 0;
2251
2254
  let copiedLayers = 0;
2255
+ let switchedLayers = 0;
2252
2256
  const layers = [];
2253
2257
  for (const [index, layer] of base.layers.entries()) {
2254
2258
  const variants = styles.map((style) => style.layers[index]);
@@ -2262,6 +2266,7 @@ function planTileflowThemeBlend(styles) {
2262
2266
  }
2263
2267
  const sky = differingRoot(styles, "sky");
2264
2268
  const light = differingRoot(styles, "light");
2269
+ const metadata = alignInteractionManifest(base.metadata, layers);
2265
2270
  return {
2266
2271
  themes: styles.length,
2267
2272
  paints,
@@ -2269,14 +2274,21 @@ function planTileflowThemeBlend(styles) {
2269
2274
  switches,
2270
2275
  ...sky === void 0 ? {} : { sky },
2271
2276
  ...light === void 0 ? {} : { light },
2272
- summary: { splitLayers, copiedLayers, switchedValues: switches.length },
2277
+ summary: { splitLayers, copiedLayers, switchedLayers, switchedValues: switches.length },
2273
2278
  styleAt(position) {
2274
2279
  const at = clampPosition(position, styles.length);
2275
2280
  const dominant = Math.round(at);
2276
2281
  const style = {
2277
2282
  ...base,
2283
+ ...metadata === void 0 ? {} : { metadata },
2284
+ // Symbols show the nearest theme, as the label cross-fade leaves them between changes.
2278
2285
  layers: layers.map(
2279
- (layer) => fillLayer(layer, at, dominant, switchesByLayer.get(String(layer.id)))
2286
+ (layer) => fillLayer(
2287
+ layer,
2288
+ symbolLayers.has(String(layer.id)) ? dominant : at,
2289
+ dominant,
2290
+ switchesByLayer.get(String(layer.id))
2291
+ )
2280
2292
  )
2281
2293
  };
2282
2294
  if (sky !== void 0) style.sky = fillTemplate(sky, at);
@@ -2301,7 +2313,7 @@ function planTileflowThemeBlend(styles) {
2301
2313
  (variant) => variant[group]?.[property]
2302
2314
  );
2303
2315
  if (values.every((value) => same(value, values[0]))) continue;
2304
- if (imageProperties.has(property)) {
2316
+ if (mixedImages.has(property)) {
2305
2317
  const tuples = imageTuples(values);
2306
2318
  if (tuples && tuples.every((names) => imageGroups.get(names[0])?.size === 1)) {
2307
2319
  for (const names of tuples) {
@@ -2345,17 +2357,46 @@ function planTileflowThemeBlend(styles) {
2345
2357
  values: variants.map((variant) => paintOf(variant)[property])
2346
2358
  });
2347
2359
  }
2360
+ if (options.featureSwitches === "layers" && results.length === 1 && ownSwitches.some((entry) => entry.values.some(isDataDriven)))
2361
+ results = switchLayer();
2348
2362
  for (const result of results) {
2349
2363
  const resultId = String(result.layer.id);
2364
+ if (type === "symbol") symbolLayers.add(resultId);
2350
2365
  const overridden = new Set(result.extra.map((entry) => entry.property));
2351
2366
  for (const entry of [
2352
2367
  ...ownPaints.filter((own) => !overridden.has(own.property)),
2353
2368
  ...result.extra
2354
2369
  ])
2355
2370
  paints.push({ layer: resultId, property: entry.property, template: entry.template, follows });
2356
- for (const entry of ownSwitches) switches.push({ layer: resultId, ...entry });
2371
+ if (result.visibility)
2372
+ switches.push({
2373
+ layer: resultId,
2374
+ group: "layout",
2375
+ property: "visibility",
2376
+ values: result.visibility
2377
+ });
2378
+ else for (const entry of ownSwitches) switches.push({ layer: resultId, ...entry });
2357
2379
  }
2358
2380
  return results.map((result) => result.layer);
2381
+ function switchLayer() {
2382
+ switchedLayers += variants.length;
2383
+ return variants.map((_2, theme) => {
2384
+ const copyPaint = { ...paint };
2385
+ const copyLayout = { ...layout };
2386
+ for (const entry of ownSwitches) {
2387
+ const target = entry.group === "paint" ? copyPaint : copyLayout;
2388
+ const value = entry.values[theme];
2389
+ if (value === void 0) delete target[entry.property];
2390
+ else target[entry.property] = value;
2391
+ }
2392
+ const shown = copyLayout.visibility === "none" ? "none" : "visible";
2393
+ return {
2394
+ layer: { ...planned, id: `${id}::theme-${theme}`, paint: copyPaint, layout: copyLayout },
2395
+ extra: [],
2396
+ visibility: variants.map((__, nearest) => nearest === theme ? shown : "none")
2397
+ };
2398
+ });
2399
+ }
2359
2400
  function splitLayer() {
2360
2401
  if (isLegacyFilter(layer.filter)) return void 0;
2361
2402
  const branches = splitBranches(
@@ -2420,6 +2461,15 @@ function planTileflowThemeBlend(styles) {
2420
2461
  function fillTileflowThemeBlendTemplate(template, position) {
2421
2462
  return fillTemplate(template, position);
2422
2463
  }
2464
+ function mixTileflowColours(from, to, t) {
2465
+ const a = oklab(from);
2466
+ const b2 = oklab(to);
2467
+ if (!a || !b2) return void 0;
2468
+ return colourFromOklab(a.map((value, channel) => value + (b2[channel] - value) * t));
2469
+ }
2470
+ function readsTileflowFeatureData(value) {
2471
+ return isDataDriven(value) || reads(value, "within") || reads(value, "distance");
2472
+ }
2423
2473
  function fillLayer(layer, position, dominant, layerSwitches) {
2424
2474
  const filled = { ...layer };
2425
2475
  for (const group of ["layout", "paint"]) {
@@ -2504,11 +2554,11 @@ function differingRoot(styles, key) {
2504
2554
  if (values.every((value) => same(value, values[0]))) return void 0;
2505
2555
  return tryMixTemplate(values);
2506
2556
  }
2507
- function collectImageNames(styles, base) {
2557
+ function collectImageNames(styles, base, properties) {
2508
2558
  const uses = /* @__PURE__ */ new Map();
2509
2559
  for (const [index] of base.layers.entries()) {
2510
2560
  for (const group of ["layout", "paint"]) {
2511
- for (const property of imageProperties) {
2561
+ for (const property of properties) {
2512
2562
  const values = styles.map(
2513
2563
  (style) => style.layers[index]?.[group]?.[property]
2514
2564
  );
@@ -2523,6 +2573,51 @@ function collectImageNames(styles, base) {
2523
2573
  }
2524
2574
  return uses;
2525
2575
  }
2576
+ function alignInteractionManifest(metadata, layers) {
2577
+ if (!isRecord2(metadata)) return void 0;
2578
+ const planned = /* @__PURE__ */ new Map();
2579
+ for (const [index, layer] of layers.entries()) {
2580
+ const id = String(layer.id);
2581
+ const original = /^(.+)::(?:branch|theme)-\d+$/u.exec(id)?.[1] ?? id;
2582
+ planned.set(original, [...planned.get(original) ?? [], { id, index }]);
2583
+ }
2584
+ let aligned;
2585
+ const interactions = metadata["tileflow:interaction-manifest"];
2586
+ const domains = isRecord2(interactions) ? interactions.domains : void 0;
2587
+ if (isRecord2(interactions) && isRecord2(domains) && isRecord2(domains.poi)) {
2588
+ const poi = domains.poi;
2589
+ if (Array.isArray(poi.layers)) {
2590
+ const poiLayers = poi.layers.flatMap((entry) => {
2591
+ if (!isRecord2(entry) || typeof entry.layerId !== "string") return [entry];
2592
+ const copies = planned.get(entry.layerId);
2593
+ if (!copies) return [entry];
2594
+ return copies.map(({ id, index }) => ({ ...entry, layerId: id, priority: index }));
2595
+ });
2596
+ aligned = {
2597
+ ...metadata,
2598
+ "tileflow:interaction-manifest": {
2599
+ ...interactions,
2600
+ domains: { ...domains, poi: { ...poi, layers: poiLayers } }
2601
+ }
2602
+ };
2603
+ }
2604
+ }
2605
+ const overlays = metadata["tileflow:overlay-placement-manifest"];
2606
+ if (isRecord2(overlays) && isRecord2(overlays.anchors)) {
2607
+ const anchors = Object.fromEntries(
2608
+ Object.entries(overlays.anchors).map(([placement, anchor]) => [
2609
+ placement,
2610
+ typeof anchor === "string" ? planned.get(anchor)?.[0]?.id ?? anchor : anchor
2611
+ ])
2612
+ );
2613
+ aligned = {
2614
+ ...aligned ?? metadata,
2615
+ "tileflow:overlay-placement-manifest": { ...overlays, anchors }
2616
+ };
2617
+ }
2618
+ return aligned;
2619
+ }
2620
+ var isRecord2 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
2526
2621
  function imageTuples(values, out = []) {
2527
2622
  const [first] = values;
2528
2623
  if (values.every((value) => typeof value === "string")) {
@@ -2789,6 +2884,196 @@ function parseUnit(part, scale) {
2789
2884
  return percentage ? value / 100 * scale : value;
2790
2885
  }
2791
2886
 
2887
+ // src/layer-domain.ts
2888
+ var tileflowLayerDomainMetadataKey = "tileflow:domain";
2889
+
2890
+ // src/theme-emphasis.ts
2891
+ var colourProperties = {
2892
+ circle: ["circle-color", "circle-stroke-color"],
2893
+ fill: ["fill-color", "fill-outline-color"],
2894
+ "fill-extrusion": ["fill-extrusion-color"],
2895
+ line: ["line-color"]
2896
+ };
2897
+ var patternProperties = {
2898
+ fill: "fill-pattern",
2899
+ "fill-extrusion": "fill-extrusion-pattern",
2900
+ line: "line-pattern"
2901
+ };
2902
+ var opacityProperties2 = {
2903
+ circle: "circle-opacity",
2904
+ fill: "fill-opacity",
2905
+ "fill-extrusion": "fill-extrusion-opacity",
2906
+ heatmap: "heatmap-opacity",
2907
+ line: "line-opacity",
2908
+ raster: "raster-opacity"
2909
+ };
2910
+ var labelProperties = ["text-opacity", "icon-opacity"];
2911
+ var maximumAreaPositions = 4096;
2912
+ function validateTileflowEmphasis(value) {
2913
+ if (!isRecord3(value)) return new TypeError("A Tileflow emphasis must be an object.");
2914
+ for (const key of Object.keys(value)) {
2915
+ if (key !== "recede" && key !== "labels" && key !== "keep") {
2916
+ return new TypeError(`A Tileflow emphasis has no ${JSON.stringify(key)} option.`);
2917
+ }
2918
+ }
2919
+ if (value.recede !== void 0) {
2920
+ if (!isRecord3(value.recede)) {
2921
+ return new TypeError("A Tileflow emphasis recede must map module names to amounts.");
2922
+ }
2923
+ for (const [module, amount] of Object.entries(value.recede)) {
2924
+ if (!/^[a-z][a-z0-9-]*$/u.test(module) || !isUnit(amount)) {
2925
+ return new TypeError(
2926
+ `A Tileflow emphasis recede amount for ${JSON.stringify(module)} must be a number from 0 to 1.`
2927
+ );
2928
+ }
2929
+ }
2930
+ }
2931
+ if (value.labels !== void 0 && !isUnit(value.labels)) {
2932
+ return new TypeError("A Tileflow emphasis labels share must be a number from 0 to 1.");
2933
+ }
2934
+ if (value.keep !== void 0) {
2935
+ const positions = areaPositions(value.keep);
2936
+ if (positions === void 0) {
2937
+ return new TypeError(
2938
+ "A Tileflow emphasis keep area must be a GeoJSON Polygon or MultiPolygon."
2939
+ );
2940
+ }
2941
+ if (positions > maximumAreaPositions) {
2942
+ return new TypeError(
2943
+ `A Tileflow emphasis keep area may hold at most ${maximumAreaPositions} positions.`
2944
+ );
2945
+ }
2946
+ }
2947
+ return void 0;
2948
+ }
2949
+ function planTileflowEmphasis(layers, emphasis) {
2950
+ const targets = [];
2951
+ const fadeLabels = emphasis.labels === void 0 ? 0 : 1 - emphasis.labels;
2952
+ for (const layer of layers) {
2953
+ if (!isRecord3(layer) || typeof layer.id !== "string" || typeof layer.type !== "string")
2954
+ continue;
2955
+ const domain = isRecord3(layer.metadata) ? layer.metadata[tileflowLayerDomainMetadataKey] : void 0;
2956
+ if (typeof domain !== "string") continue;
2957
+ const paint = isRecord3(layer.paint) ? layer.paint : {};
2958
+ const add = (property, kind, amount2) => targets.push({
2959
+ amount: amount2,
2960
+ featureData: readsTileflowFeatureData(paint[property]) || kind === "label" && emphasis.keep !== void 0,
2961
+ kind,
2962
+ layer: layer.id,
2963
+ property
2964
+ });
2965
+ if (layer.type === "symbol") {
2966
+ if (fadeLabels > 0)
2967
+ for (const property of labelProperties) add(property, "label", fadeLabels);
2968
+ continue;
2969
+ }
2970
+ if (layer.type === "background") continue;
2971
+ const amount = emphasis.recede?.[domain] ?? 0;
2972
+ if (amount <= 0) continue;
2973
+ const pattern = patternProperties[layer.type];
2974
+ const opacity = opacityProperties2[layer.type];
2975
+ if (pattern && paint[pattern] !== void 0) {
2976
+ if (opacity) add(opacity, "fade", amount);
2977
+ continue;
2978
+ }
2979
+ const colours = (colourProperties[layer.type] ?? []).filter(
2980
+ (property) => paint[property] !== void 0
2981
+ );
2982
+ if (colours.length > 0) for (const property of colours) add(property, "colour", amount);
2983
+ else if (opacity) add(opacity, "fade", amount);
2984
+ }
2985
+ return targets;
2986
+ }
2987
+ function emphasizeTileflowValue(target, value, options) {
2988
+ const amount = target.amount * Math.min(1, Math.max(0, options.strength));
2989
+ if (target.kind === "label") {
2990
+ const keep = options.keep;
2991
+ if (amount <= 0 && !keep) return value;
2992
+ const near = keep ? (original, scaled) => ["case", ["within", keep], original, scaled] : void 0;
2993
+ return scaleOpacity(value, 1 - amount, near);
2994
+ }
2995
+ if (amount <= 0) return value;
2996
+ if (target.kind === "fade") return scaleOpacity(value, 1 - amount);
2997
+ if (options.ground === void 0) return value;
2998
+ return mixColours(value, options.ground, amount);
2999
+ }
3000
+ function tileflowEmphasisGround(layers) {
3001
+ for (const layer of layers) {
3002
+ if (!isRecord3(layer) || layer.type !== "background") continue;
3003
+ const colour = isRecord3(layer.paint) ? layer.paint["background-color"] : void 0;
3004
+ return firstColour(colour);
3005
+ }
3006
+ return void 0;
3007
+ }
3008
+ function firstColour(value) {
3009
+ if (typeof value === "string") {
3010
+ return mixTileflowColours(value, value, 0) === void 0 ? void 0 : value;
3011
+ }
3012
+ if (Array.isArray(value)) {
3013
+ for (const item of value.slice(1)) {
3014
+ const found = firstColour(item);
3015
+ if (found !== void 0) return found;
3016
+ }
3017
+ }
3018
+ return void 0;
3019
+ }
3020
+ function mixColours(value, toward, amount) {
3021
+ if (typeof value === "string") return mixTileflowColours(value, toward, amount) ?? value;
3022
+ if (Array.isArray(value)) {
3023
+ if (value[0] === "literal") return value;
3024
+ return value.map((item, index) => index === 0 ? item : mixColours(item, toward, amount));
3025
+ }
3026
+ return value;
3027
+ }
3028
+ var isZoomInput = (input) => Array.isArray(input) && input[0] === "zoom";
3029
+ function scaleOpacity(value, factor, near = (_original, scaled) => scaled) {
3030
+ const scale = (item) => scaleOpacity(item, factor, near);
3031
+ if (value === void 0 || value === null) return near(1, factor);
3032
+ if (typeof value === "number") return near(value, round(value * factor));
3033
+ if (Array.isArray(value)) {
3034
+ const operator = value[0];
3035
+ if (typeof operator === "string" && operator.startsWith("interpolate") && isZoomInput(value[2])) {
3036
+ return [
3037
+ ...value.slice(0, 3),
3038
+ ...value.slice(3).map((item, index) => index % 2 === 1 ? scale(item) : item)
3039
+ ];
3040
+ }
3041
+ if (operator === "step" && isZoomInput(value[1])) {
3042
+ return [
3043
+ "step",
3044
+ value[1],
3045
+ scale(value[2]),
3046
+ ...value.slice(3).map((item, index) => index % 2 === 1 ? scale(item) : item)
3047
+ ];
3048
+ }
3049
+ if (operator === "let") return [...value.slice(0, -1), scale(value.at(-1))];
3050
+ }
3051
+ return near(value, ["*", value, factor]);
3052
+ }
3053
+ var round = (value) => Math.round(value * 1e4) / 1e4;
3054
+ function areaPositions(value) {
3055
+ if (!isRecord3(value) || !Array.isArray(value.coordinates)) return void 0;
3056
+ const polygons = value.type === "Polygon" ? [value.coordinates] : value.type === "MultiPolygon" ? value.coordinates : void 0;
3057
+ if (!polygons) return void 0;
3058
+ let positions = 0;
3059
+ for (const polygon of polygons) {
3060
+ if (!Array.isArray(polygon) || polygon.length === 0) return void 0;
3061
+ for (const ring of polygon) {
3062
+ if (!Array.isArray(ring) || ring.length < 4) return void 0;
3063
+ for (const position of ring) {
3064
+ if (!Array.isArray(position) || position.length < 2 || !position.every(
3065
+ (coordinate) => typeof coordinate === "number" && Number.isFinite(coordinate)
3066
+ ))
3067
+ return void 0;
3068
+ positions += 1;
3069
+ }
3070
+ }
3071
+ }
3072
+ return positions;
3073
+ }
3074
+ var isUnit = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
3075
+ var isRecord3 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
3076
+
2792
3077
  // src/theme-motion-browser.ts
2793
3078
  var settleFrames = 3;
2794
3079
  var settleTimeoutMs = 1e4;
@@ -2897,6 +3182,9 @@ function createTileflowThemeBlender(input, plan, options) {
2897
3182
  const dominantPaints = plan.paints.filter((paint) => paint.follows === "dominant");
2898
3183
  const pixels = createImageMixer(map, plan.images);
2899
3184
  const shownValues = /* @__PURE__ */ new Map();
3185
+ const baseValues = /* @__PURE__ */ new Map();
3186
+ const valueKey = (layer, property) => `${layer}\0${property}`;
3187
+ const transformed = (layer, property, value) => options.transform ? options.transform(layer, property, value) : value;
2900
3188
  let position = clamp(options.position);
2901
3189
  let dominant = Math.round(position);
2902
3190
  let label = dominant;
@@ -2907,7 +3195,9 @@ function createTileflowThemeBlender(input, plan, options) {
2907
3195
  pixels.paint("dominant", label);
2908
3196
  const drive = (paints, at) => {
2909
3197
  for (const paint of paints) {
2910
- const value = fillTileflowThemeBlendTemplate(paint.template, at);
3198
+ const base = fillTileflowThemeBlendTemplate(paint.template, at);
3199
+ baseValues.set(valueKey(paint.layer, paint.property), base);
3200
+ const value = transformed(paint.layer, paint.property, base);
2911
3201
  const key = JSON.stringify(value);
2912
3202
  if (shownValues.get(paint) === key) continue;
2913
3203
  shownValues.set(paint, key);
@@ -2932,18 +3222,49 @@ function createTileflowThemeBlender(input, plan, options) {
2932
3222
  const applySwitches = (theme) => {
2933
3223
  for (const entry of plan.switches) {
2934
3224
  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 });
3225
+ if (entry.group === "paint") {
3226
+ baseValues.set(valueKey(entry.layer, entry.property), value);
3227
+ map.setPaintProperty(
3228
+ entry.layer,
3229
+ entry.property,
3230
+ transformed(entry.layer, entry.property, value),
3231
+ { validate: false }
3232
+ );
3233
+ } else map.setLayoutProperty(entry.layer, entry.property, value, { validate: false });
2938
3234
  }
2939
3235
  };
3236
+ const switchedPaints = plan.switches.filter((entry) => entry.group === "paint");
2940
3237
  return {
3238
+ baseValue(layer, property) {
3239
+ return baseValues.get(valueKey(layer, property));
3240
+ },
2941
3241
  get dominant() {
2942
3242
  return dominant;
2943
3243
  },
2944
3244
  get position() {
2945
3245
  return position;
2946
3246
  },
3247
+ refresh(include) {
3248
+ if (disposed) return;
3249
+ drive(
3250
+ positionPaints.filter((paint) => include(paint.layer, paint.property)),
3251
+ position
3252
+ );
3253
+ drive(
3254
+ dominantPaints.filter((paint) => include(paint.layer, paint.property)),
3255
+ label
3256
+ );
3257
+ for (const entry of switchedPaints) {
3258
+ if (!include(entry.layer, entry.property)) continue;
3259
+ const value = entry.values[dominant];
3260
+ map.setPaintProperty(
3261
+ entry.layer,
3262
+ entry.property,
3263
+ transformed(entry.layer, entry.property, value),
3264
+ { validate: false }
3265
+ );
3266
+ }
3267
+ },
2947
3268
  dispose() {
2948
3269
  disposed = true;
2949
3270
  if (frameRequest !== void 0 && view) {
@@ -2965,6 +3286,7 @@ function createTileflowThemeBlender(input, plan, options) {
2965
3286
  map.setSky(fillTileflowThemeBlendTemplate(plan.sky, position), { validate: false });
2966
3287
  if (plan.light !== void 0 && typeof map.setLight === "function")
2967
3288
  map.setLight(fillTileflowThemeBlendTemplate(plan.light, position), { validate: false });
3289
+ options.onPosition?.();
2968
3290
  }
2969
3291
  const nextDominant = Math.round(target);
2970
3292
  if (nextDominant === dominant) return;
@@ -4751,7 +5073,7 @@ var TileflowPmtilesProtocol = class {
4751
5073
  const archive2 = this.#archive(parseArchiveTarget(request.url));
4752
5074
  const header2 = await archive2.getHeader();
4753
5075
  const tileJson = await archive2.getTileJson(request.url);
4754
- const data = header2.tileType === O.Mlt && isRecord2(tileJson) ? { ...tileJson, encoding: "mlt" } : tileJson;
5076
+ const data = header2.tileType === O.Mlt && isRecord4(tileJson) ? { ...tileJson, encoding: "mlt" } : tileJson;
4755
5077
  abortController.signal.throwIfAborted();
4756
5078
  return { data };
4757
5079
  }
@@ -4889,7 +5211,7 @@ function normalizeMaximumDirectoryDepth(value) {
4889
5211
  }
4890
5212
  return depth;
4891
5213
  }
4892
- function isRecord2(value) {
5214
+ function isRecord4(value) {
4893
5215
  return value !== null && typeof value === "object" && !Array.isArray(value);
4894
5216
  }
4895
5217
  function protocolCompatibility(handler) {
@@ -4971,11 +5293,26 @@ function createTileflowThemeController(options) {
4971
5293
  let disposed = false;
4972
5294
  let requestId = 0;
4973
5295
  let applyQueue = Promise.resolve();
5296
+ let shownLayers = typeof options.initial.style === "string" ? void 0 : options.initial.style.layers;
5297
+ let emphasis;
5298
+ let emphasisShown;
5299
+ let emphasisStrength = 0;
5300
+ let emphasisGoal = 0;
5301
+ let emphasisTargets = /* @__PURE__ */ new Map();
5302
+ let emphasisBase = /* @__PURE__ */ new Map();
5303
+ let emphasisBlended = /* @__PURE__ */ new Set();
5304
+ let emphasisGroundTemplate;
5305
+ let emphasisStaticGround;
5306
+ const emphasisWritten = /* @__PURE__ */ new Map();
5307
+ let emphasisRun = 0;
5308
+ let emphasisFrame;
5309
+ let emphasisSettle;
4974
5310
  return {
4975
5311
  dispose() {
4976
5312
  disposed = true;
4977
5313
  requestId += 1;
4978
5314
  activeBlend?.blender.dispose();
5315
+ stopEmphasisMotion({ status: "superseded", theme: current.theme });
4979
5316
  },
4980
5317
  getBlend() {
4981
5318
  if (!activeBlend) return void 0;
@@ -4987,6 +5324,21 @@ function createTileflowThemeController(options) {
4987
5324
  getCurrent() {
4988
5325
  return current;
4989
5326
  },
5327
+ getEmphasis() {
5328
+ return emphasis;
5329
+ },
5330
+ setEmphasis(request, changeOptions) {
5331
+ if (request !== void 0) {
5332
+ const invalid = validateTileflowEmphasis(request);
5333
+ if (invalid)
5334
+ return Promise.resolve({ error: invalid, status: "failed", theme: current.theme });
5335
+ }
5336
+ if (disposed) return Promise.resolve(disposedResult(current.theme));
5337
+ const transition = transitionOf(changeOptions);
5338
+ if (transition instanceof TypeError)
5339
+ return Promise.resolve({ error: transition, status: "failed", theme: current.theme });
5340
+ return changeEmphasis(request, reducedMotion() ? 0 : transition);
5341
+ },
4990
5342
  setBlend(request, changeOptions) {
4991
5343
  const targetTheme = nearestTheme(request);
4992
5344
  const invalid = validateBlendRequest(request);
@@ -5136,6 +5488,8 @@ function createTileflowThemeController(options) {
5136
5488
  current = target.themes[blender.dominant];
5137
5489
  if (pendingBlend?.key === target.key) pendingBlend = void 0;
5138
5490
  }
5491
+ shownLayers = typeof styleInput === "string" ? void 0 : styleInput.layers;
5492
+ resetEmphasis();
5139
5493
  await cover?.finish({ stop: () => disposed || runId !== requestId });
5140
5494
  options.onTransition?.({
5141
5495
  currentTheme: current.theme,
@@ -5168,7 +5522,12 @@ function createTileflowThemeController(options) {
5168
5522
  void fade?.finish();
5169
5523
  },
5170
5524
  labelFadeMs: reducedMotion() ? 0 : blendLabelFadeMs,
5171
- position
5525
+ // The ground colour moves with the blend, and colours that recede follow it.
5526
+ onPosition: () => {
5527
+ if (emphasisShown) writeEmphasis(false);
5528
+ },
5529
+ position,
5530
+ transform: (layer, property, value) => emphasisValue(layer, property, value)
5172
5531
  });
5173
5532
  }
5174
5533
  async function restore(previous) {
@@ -5179,11 +5538,193 @@ function createTileflowThemeController(options) {
5179
5538
  await applyMapStyle(options.map, style, timeoutMs);
5180
5539
  activeBlend = { ...blend, blender: createBlender(blend.plan, position) };
5181
5540
  shown = { blend: activeBlend, style };
5541
+ shownLayers = style.layers;
5182
5542
  } else {
5183
5543
  await applyMapStyle(options.map, previous.shown.style, timeoutMs);
5184
5544
  shown = previous.shown;
5545
+ shownLayers = typeof shown.style === "string" ? void 0 : shown.style.layers;
5185
5546
  }
5186
5547
  current = previous.current;
5548
+ resetEmphasis();
5549
+ }
5550
+ function valueKey(layer, property) {
5551
+ return `${layer}\0${property}`;
5552
+ }
5553
+ function layersShown() {
5554
+ if (shownLayers) return shownLayers;
5555
+ const getStyle = options.map.getStyle;
5556
+ if (typeof getStyle !== "function") return [];
5557
+ const style = getStyle.call(options.map);
5558
+ shownLayers = Array.isArray(style?.layers) ? style.layers : [];
5559
+ return shownLayers;
5560
+ }
5561
+ function planEmphasis(onMap = /* @__PURE__ */ new Map()) {
5562
+ const layers = layersShown();
5563
+ const targets = emphasisShown ? planTileflowEmphasis(layers, emphasisShown) : [];
5564
+ emphasisTargets = new Map(
5565
+ targets.map((target) => [valueKey(target.layer, target.property), target])
5566
+ );
5567
+ const paintOf = /* @__PURE__ */ new Map();
5568
+ for (const layer of layers) {
5569
+ if (layer && typeof layer === "object" && typeof layer.id === "string") {
5570
+ const paint = layer.paint;
5571
+ paintOf.set(
5572
+ layer.id,
5573
+ paint && typeof paint === "object" ? paint : {}
5574
+ );
5575
+ }
5576
+ }
5577
+ emphasisBase = new Map(
5578
+ targets.map((target) => [
5579
+ valueKey(target.layer, target.property),
5580
+ paintOf.get(target.layer)?.[target.property]
5581
+ ])
5582
+ );
5583
+ emphasisStaticGround = tileflowEmphasisGround(layers);
5584
+ emphasisBlended = new Set(
5585
+ activeBlend?.plan.paints.map((paint) => valueKey(paint.layer, paint.property)) ?? []
5586
+ );
5587
+ const background = layers.find(
5588
+ (layer) => layer && typeof layer === "object" && layer.type === "background"
5589
+ );
5590
+ emphasisGroundTemplate = activeBlend?.plan.paints.find(
5591
+ (paint) => paint.layer === background?.id && paint.property === "background-color"
5592
+ )?.template;
5593
+ emphasisWritten.clear();
5594
+ for (const [key, base] of emphasisBase) {
5595
+ emphasisWritten.set(key, onMap.get(key) ?? JSON.stringify(base ?? null));
5596
+ }
5597
+ }
5598
+ function resetEmphasis() {
5599
+ if (!emphasisShown) return;
5600
+ planEmphasis();
5601
+ writeEmphasis(true);
5602
+ }
5603
+ function emphasisGround() {
5604
+ if (activeBlend && emphasisGroundTemplate !== void 0) {
5605
+ return firstColour(
5606
+ fillTileflowThemeBlendTemplate(emphasisGroundTemplate, activeBlend.blender.position)
5607
+ );
5608
+ }
5609
+ return emphasisStaticGround;
5610
+ }
5611
+ function emphasisValue(layer, property, value) {
5612
+ const target = emphasisShown ? emphasisTargets.get(valueKey(layer, property)) : void 0;
5613
+ if (!target) return value;
5614
+ const strength = target.featureData ? emphasisGoal : emphasisStrength;
5615
+ return emphasizeTileflowValue(target, value, {
5616
+ ground: emphasisGround(),
5617
+ keep: strength > 0 ? emphasisShown?.keep : void 0,
5618
+ strength
5619
+ });
5620
+ }
5621
+ function writeEmphasis(blended) {
5622
+ if (blended && activeBlend && emphasisTargets.size > 0) {
5623
+ activeBlend.blender.refresh(
5624
+ (layer, property) => emphasisTargets.has(valueKey(layer, property))
5625
+ );
5626
+ }
5627
+ const setPaint = options.map.setPaintProperty;
5628
+ const getLayer = options.map.getLayer;
5629
+ if (typeof setPaint !== "function") return;
5630
+ for (const [key, target] of emphasisTargets) {
5631
+ if (emphasisBlended.has(key)) continue;
5632
+ const value = emphasisValue(target.layer, target.property, emphasisBase.get(key));
5633
+ const serialized = JSON.stringify(value ?? null);
5634
+ if (emphasisWritten.get(key) === serialized) continue;
5635
+ if (typeof getLayer === "function" && !getLayer.call(options.map, target.layer)) continue;
5636
+ emphasisWritten.set(key, serialized);
5637
+ setPaint.call(options.map, target.layer, target.property, value, { validate: false });
5638
+ }
5639
+ }
5640
+ function stopEmphasisMotion(result) {
5641
+ emphasisFrame?.();
5642
+ emphasisFrame = void 0;
5643
+ const settle = emphasisSettle;
5644
+ emphasisSettle = void 0;
5645
+ if (result) settle?.(result);
5646
+ }
5647
+ function changeEmphasis(request, duration) {
5648
+ stopEmphasisMotion({ status: "superseded", theme: current.theme });
5649
+ const run = ++emphasisRun;
5650
+ emphasis = request;
5651
+ const before = emphasisTargets;
5652
+ if (request) {
5653
+ const leaving = new Map(emphasisWritten);
5654
+ const previous = emphasisShown;
5655
+ emphasisShown = request;
5656
+ planEmphasis(leaving);
5657
+ if (previous) {
5658
+ const setPaint = options.map.setPaintProperty;
5659
+ for (const [key, target] of before) {
5660
+ if (emphasisTargets.has(key) || typeof setPaint !== "function") continue;
5661
+ const base = layerPaint(target.layer, target.property);
5662
+ setPaint.call(options.map, target.layer, target.property, base, { validate: false });
5663
+ }
5664
+ }
5665
+ }
5666
+ if (!emphasisShown) return Promise.resolve({ status: "applied", theme: current.theme });
5667
+ emphasisGoal = request ? 1 : 0;
5668
+ const from = emphasisStrength;
5669
+ const to = emphasisGoal;
5670
+ const switches = [...emphasisTargets.values()].some((target) => target.featureData);
5671
+ const cover = switches ? beginTileflowStyleCrossfade(options.map, duration) : void 0;
5672
+ const view = tileflowThemeMotionView(options.map);
5673
+ return new Promise((resolve) => {
5674
+ emphasisSettle = resolve;
5675
+ const finish = () => {
5676
+ emphasisFrame = void 0;
5677
+ emphasisStrength = to;
5678
+ writeEmphasis(true);
5679
+ if (to === 0) {
5680
+ emphasisShown = void 0;
5681
+ emphasisTargets = /* @__PURE__ */ new Map();
5682
+ emphasisWritten.clear();
5683
+ }
5684
+ emphasisSettle = void 0;
5685
+ resolve({ status: "applied", theme: current.theme });
5686
+ };
5687
+ writeEmphasis(true);
5688
+ void cover?.finish({ stop: () => disposed || run !== emphasisRun });
5689
+ if (duration <= 0 || !view || from === to) {
5690
+ finish();
5691
+ return;
5692
+ }
5693
+ const now = () => view.performance?.now?.() ?? Date.now();
5694
+ const start = now();
5695
+ let cancelled = false;
5696
+ let frame;
5697
+ const step = () => {
5698
+ frame = void 0;
5699
+ if (cancelled || disposed || run !== emphasisRun) return;
5700
+ const t = Math.min(1, (now() - start) / duration);
5701
+ const eased = t * t * (3 - 2 * t);
5702
+ const strength = from + (to - from) * eased;
5703
+ if (t >= 1) return finish();
5704
+ if (Math.abs(strength - emphasisStrength) >= 0.01) {
5705
+ emphasisStrength = strength;
5706
+ writeEmphasis(true);
5707
+ }
5708
+ frame = view.requestAnimationFrame(step);
5709
+ };
5710
+ emphasisFrame = () => {
5711
+ cancelled = true;
5712
+ if (frame !== void 0) view.cancelAnimationFrame(frame);
5713
+ };
5714
+ frame = view.requestAnimationFrame(step);
5715
+ });
5716
+ }
5717
+ function layerPaint(layer, property) {
5718
+ const key = valueKey(layer, property);
5719
+ if (activeBlend && emphasisBlended.has(key))
5720
+ return activeBlend.blender.baseValue(layer, property);
5721
+ for (const entry of layersShown()) {
5722
+ if (entry && typeof entry === "object" && entry.id === layer) {
5723
+ const paint = entry.paint;
5724
+ return paint?.[property];
5725
+ }
5726
+ }
5727
+ return void 0;
5187
5728
  }
5188
5729
  function reducedMotion() {
5189
5730
  const view = tileflowThemeMotionView(options.map);
@@ -5670,9 +6211,11 @@ export {
5670
6211
  registerTileflowWorldRequestBridge,
5671
6212
  subscribeTileflowSystemColorScheme,
5672
6213
  tileflowFairUseNoticeMessage,
6214
+ tileflowLayerDomainMetadataKey,
5673
6215
  tileflowMaplibreContourVersion,
5674
6216
  tileflowPmtilesMaximumDirectoryDepth,
5675
- tileflowPmtilesProtocol
6217
+ tileflowPmtilesProtocol,
6218
+ validateTileflowEmphasis
5676
6219
  };
5677
6220
  /*! Bundled license information:
5678
6221