@versatiles/svg-renderer 1.0.0 → 1.2.0

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/index.cjs CHANGED
@@ -312,10 +312,7 @@ var v8_default = {
312
312
  "type": "enum",
313
313
  "values": { "image": {} }
314
314
  },
315
- "url": {
316
- "required": true,
317
- "type": "string"
318
- },
315
+ "url": { "type": "string" },
319
316
  "coordinates": {
320
317
  "required": true,
321
318
  "type": "array",
@@ -449,19 +446,28 @@ var v8_default = {
449
446
  },
450
447
  "property-type": "data-constant"
451
448
  } },
452
- "layout_fill-extrusion": { "visibility": {
453
- "type": "enum",
454
- "values": {
455
- "visible": {},
456
- "none": {}
457
- },
458
- "default": "visible",
459
- "expression": {
460
- "interpolated": false,
461
- "parameters": ["global-state"]
449
+ "layout_fill-extrusion": {
450
+ "visibility": {
451
+ "type": "enum",
452
+ "values": {
453
+ "visible": {},
454
+ "none": {}
455
+ },
456
+ "default": "visible",
457
+ "expression": {
458
+ "interpolated": false,
459
+ "parameters": ["global-state"]
460
+ },
461
+ "property-type": "data-constant"
462
462
  },
463
- "property-type": "data-constant"
464
- } },
463
+ "fill-extrusion-rounded-corner-distance": {
464
+ "type": "number",
465
+ "default": 0,
466
+ "minimum": 0,
467
+ "units": "meters",
468
+ "property-type": "constant"
469
+ }
470
+ },
465
471
  layout_line: {
466
472
  "line-cap": {
467
473
  "type": "enum",
@@ -646,9 +652,9 @@ var v8_default = {
646
652
  "requires": ["icon-image"],
647
653
  "expression": {
648
654
  "interpolated": false,
649
- "parameters": ["zoom"]
655
+ "parameters": ["zoom", "feature"]
650
656
  },
651
- "property-type": "data-constant"
657
+ "property-type": "data-driven"
652
658
  },
653
659
  "icon-size": {
654
660
  "type": "number",
@@ -1109,6 +1115,31 @@ var v8_default = {
1109
1115
  },
1110
1116
  "property-type": "data-constant"
1111
1117
  },
1118
+ "symbol-height-offset": {
1119
+ "type": "number",
1120
+ "default": 0,
1121
+ "units": "meters",
1122
+ "requires": [{ "symbol-placement": ["point"] }],
1123
+ "expression": {
1124
+ "interpolated": true,
1125
+ "parameters": ["zoom", "feature"]
1126
+ },
1127
+ "property-type": "data-driven"
1128
+ },
1129
+ "symbol-height-anchor": {
1130
+ "type": "enum",
1131
+ "values": {
1132
+ "ground": {},
1133
+ "absolute": {}
1134
+ },
1135
+ "default": "ground",
1136
+ "requires": ["symbol-height-offset"],
1137
+ "expression": {
1138
+ "interpolated": false,
1139
+ "parameters": ["zoom"]
1140
+ },
1141
+ "property-type": "data-constant"
1142
+ },
1112
1143
  "visibility": {
1113
1144
  "type": "enum",
1114
1145
  "values": {
@@ -1584,7 +1615,6 @@ var v8_default = {
1584
1615
  "fill-extrusion-height": {
1585
1616
  "type": "number",
1586
1617
  "default": 0,
1587
- "minimum": 0,
1588
1618
  "units": "meters",
1589
1619
  "transition": true,
1590
1620
  "expression": {
@@ -1600,7 +1630,6 @@ var v8_default = {
1600
1630
  "fill-extrusion-base": {
1601
1631
  "type": "number",
1602
1632
  "default": 0,
1603
- "minimum": 0,
1604
1633
  "units": "meters",
1605
1634
  "transition": true,
1606
1635
  "requires": ["fill-extrusion-height"],
@@ -2635,44 +2664,10 @@ var v8_default = {
2635
2664
  }
2636
2665
  }
2637
2666
  };
2638
- //#endregion
2667
+
2639
2668
  //#region src/reference/latest.ts
2640
2669
  const latest = v8_default;
2641
- //#endregion
2642
- //#region src/expression/parsing_error.ts
2643
- var ExpressionParsingError = class extends Error {
2644
- constructor(key, message) {
2645
- super(message);
2646
- this.message = message;
2647
- this.key = key;
2648
- }
2649
- };
2650
- //#endregion
2651
- //#region src/expression/scope.ts
2652
- /**
2653
- * Tracks `let` bindings during expression parsing.
2654
- * @private
2655
- */
2656
- var Scope = class Scope {
2657
- constructor(parent, bindings = []) {
2658
- this.parent = parent;
2659
- this.bindings = {};
2660
- for (const [name, expression] of bindings) this.bindings[name] = expression;
2661
- }
2662
- concat(bindings) {
2663
- return new Scope(this, bindings);
2664
- }
2665
- get(name) {
2666
- if (this.bindings[name]) return this.bindings[name];
2667
- if (this.parent) return this.parent.get(name);
2668
- throw new Error(`${name} not found in scope.`);
2669
- }
2670
- has(name) {
2671
- if (this.bindings[name]) return true;
2672
- return this.parent ? this.parent.has(name) : false;
2673
- }
2674
- };
2675
- //#endregion
2670
+
2676
2671
  //#region src/expression/types.ts
2677
2672
  const NullType = { kind: "null" };
2678
2673
  const NumberType = { kind: "number" };
@@ -2768,7 +2763,7 @@ function verifyType(provided, sample) {
2768
2763
  if (provided.kind === "array" && sample.kind === "array") return provided.itemType.kind === sample.itemType.kind && typeof provided.N === "number";
2769
2764
  return provided.kind === sample.kind;
2770
2765
  }
2771
- //#endregion
2766
+
2772
2767
  //#region src/expression/types/color_spaces.ts
2773
2768
  const Xn = .96422;
2774
2769
  const Yn = 1;
@@ -2863,7 +2858,7 @@ function hslToRgb([h, s, l, alpha]) {
2863
2858
  alpha
2864
2859
  ];
2865
2860
  }
2866
- //#endregion
2861
+
2867
2862
  //#region src/util/get_own.ts
2868
2863
  const hasOwnProperty = Object.hasOwn || function hasOwnProperty(object, key) {
2869
2864
  return Object.prototype.hasOwnProperty.call(object, key);
@@ -2871,7 +2866,7 @@ const hasOwnProperty = Object.hasOwn || function hasOwnProperty(object, key) {
2871
2866
  function getOwn(object, key) {
2872
2867
  return hasOwnProperty(object, key) ? object[key] : void 0;
2873
2868
  }
2874
- //#endregion
2869
+
2875
2870
  //#region src/expression/types/parse_css_color.ts
2876
2871
  /**
2877
2872
  * CSS color parser compliant with CSS Color 4 Specification.
@@ -2950,9 +2945,9 @@ function parseCssColor(input) {
2950
2945
  const maxValue = valFormat === "%%%" ? 100 : valFormat === "" ? 255 : 0;
2951
2946
  if (maxValue) {
2952
2947
  const rgba = [
2953
- clamp(+r / maxValue, 0, 1),
2954
- clamp(+g / maxValue, 0, 1),
2955
- clamp(+b / maxValue, 0, 1),
2948
+ clamp$1(+r / maxValue, 0, 1),
2949
+ clamp$1(+g / maxValue, 0, 1),
2950
+ clamp$1(+b / maxValue, 0, 1),
2956
2951
  a ? parseAlpha(+a, ap) : 1
2957
2952
  ];
2958
2953
  if (validateNumbers(rgba)) return rgba;
@@ -2972,8 +2967,8 @@ function parseCssColor(input) {
2972
2967
  if (argFormat === " " || argFormat === " /" || argFormat === ",," || argFormat === ",,,") {
2973
2968
  const hsla = [
2974
2969
  +h,
2975
- clamp(+s, 0, 100),
2976
- clamp(+l, 0, 100),
2970
+ clamp$1(+s, 0, 100),
2971
+ clamp$1(+l, 0, 100),
2977
2972
  a ? parseAlpha(+a, ap) : 1
2978
2973
  ];
2979
2974
  if (validateNumbers(hsla)) return hslToRgb(hsla);
@@ -2984,9 +2979,9 @@ function parseHex(hex) {
2984
2979
  return parseInt(hex.padEnd(2, hex), 16) / 255;
2985
2980
  }
2986
2981
  function parseAlpha(a, asPercentage) {
2987
- return clamp(asPercentage ? a / 100 : a, 0, 1);
2982
+ return clamp$1(asPercentage ? a / 100 : a, 0, 1);
2988
2983
  }
2989
- function clamp(n, min, max) {
2984
+ function clamp$1(n, min, max) {
2990
2985
  return Math.min(Math.max(min, n), max);
2991
2986
  }
2992
2987
  /**
@@ -3749,7 +3744,7 @@ const namedColors = {
3749
3744
  50
3750
3745
  ]
3751
3746
  };
3752
- //#endregion
3747
+
3753
3748
  //#region src/util/interpolate-primitives.ts
3754
3749
  function interpolateNumber(from, to, t) {
3755
3750
  return from + t * (to - from);
@@ -3759,7 +3754,7 @@ function interpolateArray(from, to, t) {
3759
3754
  return interpolateNumber(d, to[i], t);
3760
3755
  });
3761
3756
  }
3762
- //#endregion
3757
+
3763
3758
  //#region src/expression/types/color.ts
3764
3759
  /**
3765
3760
  * Checks whether the specified color space is one of the supported interpolation color spaces.
@@ -3943,26 +3938,7 @@ var Color$1 = class Color {
3943
3938
  }
3944
3939
  }
3945
3940
  };
3946
- //#endregion
3947
- //#region src/expression/types/collator.ts
3948
- var Collator = class {
3949
- constructor(caseSensitive, diacriticSensitive, locale) {
3950
- if (caseSensitive) this.sensitivity = diacriticSensitive ? "variant" : "case";
3951
- else this.sensitivity = diacriticSensitive ? "accent" : "base";
3952
- this.locale = locale;
3953
- this.collator = new Intl.Collator(this.locale ? this.locale : [], {
3954
- sensitivity: this.sensitivity,
3955
- usage: "search"
3956
- });
3957
- }
3958
- compare(lhs, rhs) {
3959
- return this.collator.compare(lhs, rhs);
3960
- }
3961
- resolvedLocale() {
3962
- return new Intl.Collator(this.locale ? this.locale : []).resolvedOptions().locale;
3963
- }
3964
- };
3965
- //#endregion
3941
+
3966
3942
  //#region src/expression/types/formatted.ts
3967
3943
  const VERTICAL_ALIGN_OPTIONS = [
3968
3944
  "bottom",
@@ -3999,7 +3975,7 @@ var Formatted = class Formatted {
3999
3975
  return this.sections.map((section) => section.text).join("");
4000
3976
  }
4001
3977
  };
4002
- //#endregion
3978
+
4003
3979
  //#region src/expression/types/padding.ts
4004
3980
  /**
4005
3981
  * A set of four numbers representing padding around a box. Create instances from
@@ -4043,14 +4019,12 @@ var Padding = class Padding {
4043
4019
  input[1]
4044
4020
  ];
4045
4021
  break;
4046
- case 3:
4047
- input = [
4048
- input[0],
4049
- input[1],
4050
- input[2],
4051
- input[1]
4052
- ];
4053
- break;
4022
+ case 3: input = [
4023
+ input[0],
4024
+ input[1],
4025
+ input[2],
4026
+ input[1]
4027
+ ];
4054
4028
  }
4055
4029
  return new Padding(input);
4056
4030
  }
@@ -4061,7 +4035,7 @@ var Padding = class Padding {
4061
4035
  return new Padding(interpolateArray(from.values, to.values, t));
4062
4036
  }
4063
4037
  };
4064
- //#endregion
4038
+
4065
4039
  //#region src/expression/types/number_array.ts
4066
4040
  /**
4067
4041
  * An array of numbers. Create instances from
@@ -4091,7 +4065,7 @@ var NumberArray = class NumberArray {
4091
4065
  return new NumberArray(interpolateArray(from.values, to.values, t));
4092
4066
  }
4093
4067
  };
4094
- //#endregion
4068
+
4095
4069
  //#region src/expression/types/color_array.ts
4096
4070
  /**
4097
4071
  * An array of colors. Create instances from
@@ -4134,7 +4108,7 @@ var ColorArray = class ColorArray {
4134
4108
  return new ColorArray(colors);
4135
4109
  }
4136
4110
  };
4137
- //#endregion
4111
+
4138
4112
  //#region src/expression/runtime_error.ts
4139
4113
  var RuntimeError = class extends Error {
4140
4114
  constructor(message, path) {
@@ -4146,7 +4120,7 @@ var RuntimeError = class extends Error {
4146
4120
  return this.message;
4147
4121
  }
4148
4122
  };
4149
- //#endregion
4123
+
4150
4124
  //#region src/expression/types/variable_anchor_offset_collection.ts
4151
4125
  /** Set of valid anchor positions, as a set for validation */
4152
4126
  const anchors = /* @__PURE__ */ new Set([
@@ -4198,7 +4172,7 @@ var VariableAnchorOffsetCollection = class VariableAnchorOffsetCollection {
4198
4172
  return new VariableAnchorOffsetCollection(output);
4199
4173
  }
4200
4174
  };
4201
- //#endregion
4175
+
4202
4176
  //#region src/expression/types/resolved_image.ts
4203
4177
  var ResolvedImage = class ResolvedImage {
4204
4178
  constructor(options) {
@@ -4216,7 +4190,7 @@ var ResolvedImage = class ResolvedImage {
4216
4190
  });
4217
4191
  }
4218
4192
  };
4219
- //#endregion
4193
+
4220
4194
  //#region src/expression/types/projection_definition.ts
4221
4195
  var ProjectionDefinition = class ProjectionDefinition {
4222
4196
  constructor(from, to, transition) {
@@ -4242,7 +4216,26 @@ var ProjectionDefinition = class ProjectionDefinition {
4242
4216
  if (typeof input === "string") return new ProjectionDefinition(input, input, 1);
4243
4217
  }
4244
4218
  };
4245
- //#endregion
4219
+
4220
+ //#region src/expression/types/collator.ts
4221
+ var Collator = class {
4222
+ constructor(caseSensitive, diacriticSensitive, locale) {
4223
+ if (caseSensitive) this.sensitivity = diacriticSensitive ? "variant" : "case";
4224
+ else this.sensitivity = diacriticSensitive ? "accent" : "base";
4225
+ this.locale = locale;
4226
+ this.collator = new Intl.Collator(this.locale ? this.locale : [], {
4227
+ sensitivity: this.sensitivity,
4228
+ usage: "search"
4229
+ });
4230
+ }
4231
+ compare(lhs, rhs) {
4232
+ return this.collator.compare(lhs, rhs);
4233
+ }
4234
+ resolvedLocale() {
4235
+ return new Intl.Collator(this.locale ? this.locale : []).resolvedOptions().locale;
4236
+ }
4237
+ };
4238
+
4246
4239
  //#region src/expression/values.ts
4247
4240
  function validateRGBA(r, g, b, a) {
4248
4241
  if (!(typeof r === "number" && r >= 0 && r <= 255 && typeof g === "number" && g >= 0 && g <= 255 && typeof b === "number" && b >= 0 && b <= 255)) return `Invalid rgba value [${(typeof a === "number" ? [
@@ -4309,7 +4302,7 @@ function valueToString(value) {
4309
4302
  else if (value instanceof Color$1 || value instanceof ProjectionDefinition || value instanceof Formatted || value instanceof Padding || value instanceof NumberArray || value instanceof ColorArray || value instanceof VariableAnchorOffsetCollection || value instanceof ResolvedImage) return value.toString();
4310
4303
  else return JSON.stringify(value);
4311
4304
  }
4312
- //#endregion
4305
+
4313
4306
  //#region src/expression/definitions/literal.ts
4314
4307
  var Literal = class Literal {
4315
4308
  constructor(type, value) {
@@ -4318,7 +4311,7 @@ var Literal = class Literal {
4318
4311
  }
4319
4312
  static parse(args, context) {
4320
4313
  if (args.length !== 2) return context.error(`'literal' expression requires exactly one argument, but found ${args.length - 1} instead.`);
4321
- if (!isValue(args[1])) return context.error("invalid value");
4314
+ if (!isValue(args[1])) return context.error(`invalid value of type "${typeof args[1]}"`);
4322
4315
  const value = args[1];
4323
4316
  let type = typeOf(value);
4324
4317
  const expected = context.expectedType;
@@ -4333,85 +4326,524 @@ var Literal = class Literal {
4333
4326
  return true;
4334
4327
  }
4335
4328
  };
4336
- //#endregion
4337
- //#region src/expression/definitions/assertion.ts
4338
- const types$1 = {
4339
- string: StringType,
4340
- number: NumberType,
4341
- boolean: BooleanType,
4342
- object: ObjectType
4343
- };
4344
- var Assertion = class Assertion {
4345
- constructor(type, args, key) {
4346
- this.type = type;
4347
- this.args = args;
4348
- this.key = key;
4329
+
4330
+ //#region src/expression/evaluation_context.ts
4331
+ const geometryTypes = [
4332
+ "Unknown",
4333
+ "Point",
4334
+ "LineString",
4335
+ "Polygon"
4336
+ ];
4337
+ var EvaluationContext = class {
4338
+ constructor() {
4339
+ this.globals = null;
4340
+ this.feature = null;
4341
+ this.featureState = null;
4342
+ this.formattedSection = null;
4343
+ this._parseColorCache = /* @__PURE__ */ new Map();
4344
+ this.availableImages = null;
4345
+ this.canonical = null;
4349
4346
  }
4350
- static parse(args, context) {
4351
- if (args.length < 2) return context.error("Expected at least one argument.");
4352
- let i = 1;
4353
- let type;
4354
- const name = args[0];
4355
- if (name === "array") {
4356
- let itemType;
4357
- if (args.length > 2) {
4358
- const type = args[1];
4359
- if (typeof type !== "string" || !(type in types$1) || type === "object") return context.error("The item type argument of \"array\" must be one of string, number, boolean", 1);
4360
- itemType = types$1[type];
4361
- i++;
4362
- } else itemType = ValueType;
4363
- let N;
4364
- if (args.length > 3) {
4365
- if (args[2] !== null && (typeof args[2] !== "number" || args[2] < 0 || args[2] !== Math.floor(args[2]))) return context.error("The length argument to \"array\" must be a positive integer literal", 2);
4366
- N = args[2];
4367
- i++;
4368
- }
4369
- type = array(itemType, N);
4370
- } else {
4371
- if (!types$1[name]) throw new Error(`Types doesn't contain name = ${name}`);
4372
- type = types$1[name];
4373
- }
4374
- const parsed = [];
4375
- for (; i < args.length; i++) {
4376
- const input = context.parse(args[i], i, ValueType);
4377
- if (!input) return null;
4378
- parsed.push(input);
4379
- }
4380
- return new Assertion(type, parsed, context.key);
4347
+ id() {
4348
+ return this.feature && "id" in this.feature ? this.feature.id : null;
4381
4349
  }
4382
- evaluate(ctx) {
4383
- for (let i = 0; i < this.args.length; i++) {
4384
- const value = this.args[i].evaluate(ctx);
4385
- if (!checkSubtype(this.type, typeOf(value))) return value;
4386
- else if (i === this.args.length - 1) throw new RuntimeError(`Expected value to be of type ${typeToString(this.type)}, but found ${typeToString(typeOf(value))} instead.`, this.key);
4387
- }
4388
- throw new Error();
4350
+ geometryType() {
4351
+ return this.feature ? typeof this.feature.type === "number" ? geometryTypes[this.feature.type] : this.feature.type : null;
4389
4352
  }
4390
- eachChild(fn) {
4391
- this.args.forEach(fn);
4353
+ geometry() {
4354
+ return this.feature && "geometry" in this.feature ? this.feature.geometry : null;
4392
4355
  }
4393
- outputDefined() {
4394
- return this.args.every((arg) => arg.outputDefined());
4356
+ canonicalID() {
4357
+ return this.canonical;
4358
+ }
4359
+ properties() {
4360
+ return this.feature && this.feature.properties || {};
4361
+ }
4362
+ parseColor(input) {
4363
+ let cached = this._parseColorCache.get(input);
4364
+ if (!cached) {
4365
+ cached = Color$1.parse(input);
4366
+ this._parseColorCache.set(input, cached);
4367
+ }
4368
+ return cached;
4395
4369
  }
4396
4370
  };
4397
- //#endregion
4398
- //#region src/expression/definitions/coercion.ts
4399
- const types = {
4400
- "to-boolean": BooleanType,
4401
- "to-color": ColorType,
4402
- "to-number": NumberType,
4403
- "to-string": StringType
4404
- };
4371
+
4372
+ //#region src/expression/stops.ts
4405
4373
  /**
4406
- * Special form for error-coalescing coercion expressions "to-number",
4407
- * "to-color". Since these coercions can fail at runtime, they accept multiple
4408
- * arguments, only evaluating one at a time until one succeeds.
4409
- *
4374
+ * Returns the index of the last stop <= input, or 0 if it doesn't exist.
4410
4375
  * @private
4411
4376
  */
4412
- var Coercion = class Coercion {
4413
- constructor(type, args, key) {
4414
- this.type = type;
4377
+ function findStopLessThanOrEqualTo(stops, input, key) {
4378
+ const lastIndex = stops.length - 1;
4379
+ let lowerIndex = 0;
4380
+ let upperIndex = lastIndex;
4381
+ let currentIndex = 0;
4382
+ let currentValue, nextValue;
4383
+ while (lowerIndex <= upperIndex) {
4384
+ currentIndex = Math.floor((lowerIndex + upperIndex) / 2);
4385
+ currentValue = stops[currentIndex];
4386
+ nextValue = stops[currentIndex + 1];
4387
+ if (currentValue <= input) {
4388
+ if (currentIndex === lastIndex || input < nextValue) return currentIndex;
4389
+ lowerIndex = currentIndex + 1;
4390
+ } else if (currentValue > input) upperIndex = currentIndex - 1;
4391
+ else throw new RuntimeError("Input is not a number.", key);
4392
+ }
4393
+ return 0;
4394
+ }
4395
+
4396
+ //#region src/expression/definitions/step.ts
4397
+ var Step = class Step {
4398
+ constructor(type, input, stops, key) {
4399
+ this.type = type;
4400
+ this.input = input;
4401
+ this.key = key;
4402
+ this.labels = [];
4403
+ this.outputs = [];
4404
+ for (const [label, expression] of stops) {
4405
+ this.labels.push(label);
4406
+ this.outputs.push(expression);
4407
+ }
4408
+ }
4409
+ static parse(args, context) {
4410
+ if (args.length - 1 < 4) return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`);
4411
+ if ((args.length - 1) % 2 !== 0) return context.error("Expected an even number of arguments.");
4412
+ const input = context.parse(args[1], 1, NumberType);
4413
+ if (!input) return null;
4414
+ const stops = [];
4415
+ let outputType = null;
4416
+ if (context.expectedType && context.expectedType.kind !== "value") outputType = context.expectedType;
4417
+ for (let i = 1; i < args.length; i += 2) {
4418
+ const label = i === 1 ? -Infinity : args[i];
4419
+ const value = args[i + 1];
4420
+ const labelKey = i;
4421
+ const valueKey = i + 1;
4422
+ if (typeof label !== "number") return context.error("Input/output pairs for \"step\" expressions must be defined using literal numeric values (not computed expressions) for the input values.", labelKey);
4423
+ if (stops.length && stops[stops.length - 1][0] >= label) return context.error("Input/output pairs for \"step\" expressions must be arranged with input values in strictly ascending order.", labelKey);
4424
+ const parsed = context.parse(value, valueKey, outputType);
4425
+ if (!parsed) return null;
4426
+ outputType = outputType || parsed.type;
4427
+ stops.push([label, parsed]);
4428
+ }
4429
+ return new Step(outputType, input, stops, context.key);
4430
+ }
4431
+ evaluate(ctx) {
4432
+ const labels = this.labels;
4433
+ const outputs = this.outputs;
4434
+ if (labels.length === 1) return outputs[0].evaluate(ctx);
4435
+ const value = this.input.evaluate(ctx);
4436
+ if (value <= labels[0]) return outputs[0].evaluate(ctx);
4437
+ const stopCount = labels.length;
4438
+ if (value >= labels[stopCount - 1]) return outputs[stopCount - 1].evaluate(ctx);
4439
+ return outputs[findStopLessThanOrEqualTo(labels, value, this.key)].evaluate(ctx);
4440
+ }
4441
+ eachChild(fn) {
4442
+ fn(this.input);
4443
+ for (const expression of this.outputs) fn(expression);
4444
+ }
4445
+ outputDefined() {
4446
+ return this.outputs.every((out) => out.outputDefined());
4447
+ }
4448
+ };
4449
+
4450
+ //#region node_modules/@mapbox/unitbezier/index.js
4451
+ function unitBezier(p1x, p1y, p2x, p2y) {
4452
+ const cx = 3 * p1x;
4453
+ const bx = 3 * (p2x - p1x) - cx;
4454
+ const ax = 1 - cx - bx;
4455
+ const cy = 3 * p1y;
4456
+ const by = 3 * (p2y - p1y) - cy;
4457
+ const ay = 1 - cy - by;
4458
+ return function solve(x, epsilon = 1e-6) {
4459
+ if (x <= 0) return 0;
4460
+ if (x >= 1) return 1;
4461
+ let t = x;
4462
+ for (let i = 0; i < 8; i++) {
4463
+ const x2 = ((ax * t + bx) * t + cx) * t - x;
4464
+ if (Math.abs(x2) < epsilon) return ((ay * t + by) * t + cy) * t;
4465
+ const d2 = (3 * ax * t + 2 * bx) * t + cx;
4466
+ if (Math.abs(d2) < 1e-6) break;
4467
+ t -= x2 / d2;
4468
+ }
4469
+ let t0 = 0;
4470
+ let t1 = 1;
4471
+ t = x;
4472
+ for (let i = 0; i < 20; i++) {
4473
+ const x2 = ((ax * t + bx) * t + cx) * t;
4474
+ if (Math.abs(x2 - x) < epsilon) break;
4475
+ if (x > x2) t0 = t;
4476
+ else t1 = t;
4477
+ t = (t0 + t1) * .5;
4478
+ }
4479
+ return ((ay * t + by) * t + cy) * t;
4480
+ };
4481
+ }
4482
+
4483
+ //#region src/expression/definitions/interpolate.ts
4484
+ var Interpolate = class Interpolate {
4485
+ constructor(type, operator, interpolation, input, stops, key) {
4486
+ this.type = type;
4487
+ this.operator = operator;
4488
+ this.interpolation = interpolation;
4489
+ this.input = input;
4490
+ this.key = key;
4491
+ this.labels = [];
4492
+ this.outputs = [];
4493
+ for (const [label, expression] of stops) {
4494
+ this.labels.push(label);
4495
+ this.outputs.push(expression);
4496
+ }
4497
+ }
4498
+ static interpolationFactor(interpolation, input, lower, upper) {
4499
+ let t = 0;
4500
+ if (interpolation.name === "exponential") t = exponentialInterpolation(input, interpolation.base, lower, upper);
4501
+ else if (interpolation.name === "linear") t = exponentialInterpolation(input, 1, lower, upper);
4502
+ else if (interpolation.name === "cubic-bezier") {
4503
+ const c = interpolation.controlPoints;
4504
+ t = unitBezier(c[0], c[1], c[2], c[3])(exponentialInterpolation(input, 1, lower, upper));
4505
+ }
4506
+ return t;
4507
+ }
4508
+ static parse(args, context) {
4509
+ let [operator, interpolation, input, ...rest] = args;
4510
+ if (!Array.isArray(interpolation) || interpolation.length === 0) return context.error("Expected an interpolation type expression.", 1);
4511
+ if (interpolation[0] === "linear") interpolation = { name: "linear" };
4512
+ else if (interpolation[0] === "exponential") {
4513
+ const base = interpolation[1];
4514
+ if (typeof base !== "number") return context.error("Exponential interpolation requires a numeric base.", 1, 1);
4515
+ interpolation = {
4516
+ name: "exponential",
4517
+ base
4518
+ };
4519
+ } else if (interpolation[0] === "cubic-bezier") {
4520
+ const controlPoints = interpolation.slice(1);
4521
+ if (controlPoints.length !== 4 || controlPoints.some((t) => typeof t !== "number" || t < 0 || t > 1)) return context.error("Cubic bezier interpolation requires four numeric arguments with values between 0 and 1.", 1);
4522
+ interpolation = {
4523
+ name: "cubic-bezier",
4524
+ controlPoints
4525
+ };
4526
+ } else return context.error(`Unknown interpolation type ${String(interpolation[0])}`, 1, 0);
4527
+ if (args.length - 1 < 4) return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`);
4528
+ if ((args.length - 1) % 2 !== 0) return context.error("Expected an even number of arguments.");
4529
+ input = context.parse(input, 2, NumberType);
4530
+ if (!input) return null;
4531
+ const stops = [];
4532
+ let outputType = null;
4533
+ if ((operator === "interpolate-hcl" || operator === "interpolate-lab") && context.expectedType != ColorArrayType) outputType = ColorType;
4534
+ else if (context.expectedType && context.expectedType.kind !== "value") outputType = context.expectedType;
4535
+ for (let i = 0; i < rest.length; i += 2) {
4536
+ const label = rest[i];
4537
+ const value = rest[i + 1];
4538
+ const labelKey = i + 3;
4539
+ const valueKey = i + 4;
4540
+ if (typeof label !== "number") return context.error("Input/output pairs for \"interpolate\" expressions must be defined using literal numeric values (not computed expressions) for the input values.", labelKey);
4541
+ if (stops.length && stops[stops.length - 1][0] >= label) return context.error("Input/output pairs for \"interpolate\" expressions must be arranged with input values in strictly ascending order.", labelKey);
4542
+ const parsed = context.parse(value, valueKey, outputType);
4543
+ if (!parsed) return null;
4544
+ outputType = outputType || parsed.type;
4545
+ stops.push([label, parsed]);
4546
+ }
4547
+ if (!verifyType(outputType, NumberType) && !verifyType(outputType, ProjectionDefinitionType) && !verifyType(outputType, ColorType) && !verifyType(outputType, PaddingType) && !verifyType(outputType, NumberArrayType) && !verifyType(outputType, ColorArrayType) && !verifyType(outputType, VariableAnchorOffsetCollectionType) && !verifyType(outputType, array(NumberType))) return context.error(`Type ${typeToString(outputType)} is not interpolatable.`);
4548
+ return new Interpolate(outputType, operator, interpolation, input, stops, context.key);
4549
+ }
4550
+ evaluate(ctx) {
4551
+ const labels = this.labels;
4552
+ const outputs = this.outputs;
4553
+ if (labels.length === 1) return outputs[0].evaluate(ctx);
4554
+ const value = this.input.evaluate(ctx);
4555
+ if (value <= labels[0]) return outputs[0].evaluate(ctx);
4556
+ const stopCount = labels.length;
4557
+ if (value >= labels[stopCount - 1]) return outputs[stopCount - 1].evaluate(ctx);
4558
+ const index = findStopLessThanOrEqualTo(labels, value, this.key);
4559
+ const lower = labels[index];
4560
+ const upper = labels[index + 1];
4561
+ const t = Interpolate.interpolationFactor(this.interpolation, value, lower, upper);
4562
+ const outputLower = outputs[index].evaluate(ctx);
4563
+ const outputUpper = outputs[index + 1].evaluate(ctx);
4564
+ switch (this.operator) {
4565
+ case "interpolate": switch (this.type.kind) {
4566
+ case "number": return interpolateNumber(outputLower, outputUpper, t);
4567
+ case "color": return Color$1.interpolate(outputLower, outputUpper, t);
4568
+ case "padding": return Padding.interpolate(outputLower, outputUpper, t);
4569
+ case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t);
4570
+ case "numberArray": return NumberArray.interpolate(outputLower, outputUpper, t);
4571
+ case "variableAnchorOffsetCollection": return VariableAnchorOffsetCollection.interpolate(outputLower, outputUpper, t, this.key);
4572
+ case "array": return interpolateArray(outputLower, outputUpper, t);
4573
+ case "projectionDefinition": return ProjectionDefinition.interpolate(outputLower, outputUpper, t);
4574
+ }
4575
+ case "interpolate-hcl": switch (this.type.kind) {
4576
+ case "color": return Color$1.interpolate(outputLower, outputUpper, t, "hcl");
4577
+ case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t, "hcl");
4578
+ }
4579
+ case "interpolate-lab": switch (this.type.kind) {
4580
+ case "color": return Color$1.interpolate(outputLower, outputUpper, t, "lab");
4581
+ case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t, "lab");
4582
+ }
4583
+ }
4584
+ }
4585
+ eachChild(fn) {
4586
+ fn(this.input);
4587
+ for (const expression of this.outputs) fn(expression);
4588
+ }
4589
+ outputDefined() {
4590
+ return this.outputs.every((out) => out.outputDefined());
4591
+ }
4592
+ };
4593
+ /**
4594
+ * Returns a ratio that can be used to interpolate between exponential function
4595
+ * stops.
4596
+ * How it works: Two consecutive stop values define a (scaled and shifted) exponential function `f(x) = a * base^x + b`, where `base` is the user-specified base,
4597
+ * and `a` and `b` are constants affording sufficient degrees of freedom to fit
4598
+ * the function to the given stops.
4599
+ *
4600
+ * Here's a bit of algebra that lets us compute `f(x)` directly from the stop
4601
+ * values without explicitly solving for `a` and `b`:
4602
+ *
4603
+ * First stop value: `f(x0) = y0 = a * base^x0 + b`
4604
+ * Second stop value: `f(x1) = y1 = a * base^x1 + b`
4605
+ * => `y1 - y0 = a(base^x1 - base^x0)`
4606
+ * => `a = (y1 - y0)/(base^x1 - base^x0)`
4607
+ *
4608
+ * Desired value: `f(x) = y = a * base^x + b`
4609
+ * => `f(x) = y0 + a * (base^x - base^x0)`
4610
+ *
4611
+ * From the above, we can replace the `a` in `a * (base^x - base^x0)` and do a
4612
+ * little algebra:
4613
+ * ```
4614
+ * a * (base^x - base^x0) = (y1 - y0)/(base^x1 - base^x0) * (base^x - base^x0)
4615
+ * = (y1 - y0) * (base^x - base^x0) / (base^x1 - base^x0)
4616
+ * ```
4617
+ *
4618
+ * If we let `(base^x - base^x0) / (base^x1 base^x0)`, then we have
4619
+ * `f(x) = y0 + (y1 - y0) * ratio`. In other words, `ratio` may be treated as
4620
+ * an interpolation factor between the two stops' output values.
4621
+ *
4622
+ * (Note: a slightly different form for `ratio`,
4623
+ * `(base^(x-x0) - 1) / (base^(x1-x0) - 1) `, is equivalent, but requires fewer
4624
+ * expensive `Math.pow()` operations.)
4625
+ *
4626
+ * @private
4627
+ */
4628
+ function exponentialInterpolation(input, base, lowerValue, upperValue) {
4629
+ const difference = upperValue - lowerValue;
4630
+ const progress = input - lowerValue;
4631
+ if (difference === 0) return 0;
4632
+ else if (base === 1) return progress / difference;
4633
+ else return (Math.pow(base, progress) - 1) / (Math.pow(base, difference) - 1);
4634
+ }
4635
+ const interpolateFactory = {
4636
+ color: Color$1.interpolate,
4637
+ number: interpolateNumber,
4638
+ padding: Padding.interpolate,
4639
+ numberArray: NumberArray.interpolate,
4640
+ colorArray: ColorArray.interpolate,
4641
+ variableAnchorOffsetCollection: VariableAnchorOffsetCollection.interpolate,
4642
+ array: interpolateArray
4643
+ };
4644
+
4645
+ //#region src/expression/definitions/format.ts
4646
+ var FormatExpression = class FormatExpression {
4647
+ constructor(sections) {
4648
+ this.type = FormattedType;
4649
+ this.sections = sections;
4650
+ }
4651
+ static parse(args, context) {
4652
+ if (args.length < 2) return context.error("Expected at least one argument.");
4653
+ const firstArg = args[1];
4654
+ if (!Array.isArray(firstArg) && typeof firstArg === "object") return context.error("First argument must be an image or text section.");
4655
+ const sections = [];
4656
+ let nextTokenMayBeObject = false;
4657
+ for (let i = 1; i <= args.length - 1; ++i) {
4658
+ const arg = args[i];
4659
+ if (nextTokenMayBeObject && typeof arg === "object" && !Array.isArray(arg)) {
4660
+ nextTokenMayBeObject = false;
4661
+ let scale = null;
4662
+ if (arg["font-scale"]) {
4663
+ scale = context.parse(arg["font-scale"], 1, NumberType);
4664
+ if (!scale) return null;
4665
+ }
4666
+ let font = null;
4667
+ if (arg["text-font"]) {
4668
+ font = context.parse(arg["text-font"], 1, array(StringType));
4669
+ if (!font) return null;
4670
+ }
4671
+ let textColor = null;
4672
+ if (arg["text-color"]) {
4673
+ textColor = context.parse(arg["text-color"], 1, ColorType);
4674
+ if (!textColor) return null;
4675
+ }
4676
+ let verticalAlign = null;
4677
+ if (arg["vertical-align"]) {
4678
+ if (typeof arg["vertical-align"] === "string" && !VERTICAL_ALIGN_OPTIONS.includes(arg["vertical-align"])) return context.error(`'vertical-align' must be one of: 'bottom', 'center', 'top' but found '${arg["vertical-align"]}' instead.`);
4679
+ verticalAlign = context.parse(arg["vertical-align"], 1, StringType);
4680
+ if (!verticalAlign) return null;
4681
+ }
4682
+ const lastExpression = sections[sections.length - 1];
4683
+ lastExpression.scale = scale;
4684
+ lastExpression.font = font;
4685
+ lastExpression.textColor = textColor;
4686
+ lastExpression.verticalAlign = verticalAlign;
4687
+ } else {
4688
+ const content = context.parse(args[i], 1, ValueType);
4689
+ if (!content) return null;
4690
+ const kind = content.type.kind;
4691
+ if (kind !== "string" && kind !== "value" && kind !== "null" && kind !== "resolvedImage") return context.error("Formatted text type must be 'string', 'value', 'image' or 'null'.");
4692
+ nextTokenMayBeObject = true;
4693
+ sections.push({
4694
+ content,
4695
+ scale: null,
4696
+ font: null,
4697
+ textColor: null,
4698
+ verticalAlign: null
4699
+ });
4700
+ }
4701
+ }
4702
+ return new FormatExpression(sections);
4703
+ }
4704
+ evaluate(ctx) {
4705
+ const evaluateSection = (section) => {
4706
+ const evaluatedContent = section.content.evaluate(ctx);
4707
+ if (typeOf(evaluatedContent) === ResolvedImageType) return new FormattedSection("", evaluatedContent, null, null, null, section.verticalAlign ? section.verticalAlign.evaluate(ctx) : null);
4708
+ return new FormattedSection(valueToString(evaluatedContent), null, section.scale ? section.scale.evaluate(ctx) : null, section.font ? section.font.evaluate(ctx).join(",") : null, section.textColor ? section.textColor.evaluate(ctx) : null, section.verticalAlign ? section.verticalAlign.evaluate(ctx) : null);
4709
+ };
4710
+ return new Formatted(this.sections.map(evaluateSection));
4711
+ }
4712
+ eachChild(fn) {
4713
+ for (const section of this.sections) {
4714
+ fn(section.content);
4715
+ if (section.scale) fn(section.scale);
4716
+ if (section.font) fn(section.font);
4717
+ if (section.textColor) fn(section.textColor);
4718
+ if (section.verticalAlign) fn(section.verticalAlign);
4719
+ }
4720
+ }
4721
+ outputDefined() {
4722
+ return false;
4723
+ }
4724
+ };
4725
+
4726
+ //#region src/util/classify_rings.ts
4727
+ /**
4728
+ * Classifies an array of rings into polygons with outer rings and holes
4729
+ * @param rings - the rings to classify
4730
+ * @param maxRings - the maximum number of rings to include in a polygon, use 0 to include all rings
4731
+ * @returns an array of polygons with internal rings as holes
4732
+ */
4733
+ function classifyRings$1(rings, maxRings) {
4734
+ if (rings.length <= 1) return [rings];
4735
+ const polygons = [];
4736
+ let polygon;
4737
+ let ccw;
4738
+ for (const ring of rings) {
4739
+ const area = calculateSignedArea(ring);
4740
+ if (area === 0) continue;
4741
+ ring.area = Math.abs(area);
4742
+ if (ccw === void 0) ccw = area < 0;
4743
+ if (ccw === area < 0) {
4744
+ if (polygon) polygons.push(polygon);
4745
+ polygon = [ring];
4746
+ } else polygon.push(ring);
4747
+ }
4748
+ if (polygon) polygons.push(polygon);
4749
+ return polygons;
4750
+ }
4751
+ /**
4752
+ * Returns the signed area for the polygon ring. Positive areas are exterior rings and
4753
+ * have a clockwise winding. Negative areas are interior rings and have a counter clockwise
4754
+ * ordering.
4755
+ *
4756
+ * @param ring - Exterior or interior ring
4757
+ * @returns Signed area
4758
+ */
4759
+ function calculateSignedArea(ring) {
4760
+ let sum = 0;
4761
+ for (let i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) {
4762
+ p1 = ring[i];
4763
+ p2 = ring[j];
4764
+ sum += (p2.x - p1.x) * (p1.y + p2.y);
4765
+ }
4766
+ return sum;
4767
+ }
4768
+
4769
+ //#region src/expression/definitions/assertion.ts
4770
+ const types$1 = {
4771
+ string: StringType,
4772
+ number: NumberType,
4773
+ boolean: BooleanType,
4774
+ object: ObjectType
4775
+ };
4776
+ var Assertion = class Assertion {
4777
+ constructor(type, args, key) {
4778
+ this.type = type;
4779
+ this.args = args;
4780
+ this.key = key;
4781
+ }
4782
+ static parse(args, context) {
4783
+ if (args.length < 2) return context.error("Expected at least one argument.");
4784
+ let i = 1;
4785
+ let type;
4786
+ const name = args[0];
4787
+ if (name === "array") {
4788
+ let itemType;
4789
+ if (args.length > 2) {
4790
+ const type = args[1];
4791
+ if (typeof type !== "string" || !(type in types$1) || type === "object") return context.error("The item type argument of \"array\" must be one of string, number, boolean", 1);
4792
+ itemType = types$1[type];
4793
+ i++;
4794
+ } else itemType = ValueType;
4795
+ let N;
4796
+ if (args.length > 3) {
4797
+ if (args[2] !== null && (typeof args[2] !== "number" || args[2] < 0 || args[2] !== Math.floor(args[2]))) return context.error("The length argument to \"array\" must be a positive integer literal", 2);
4798
+ N = args[2];
4799
+ i++;
4800
+ }
4801
+ type = array(itemType, N);
4802
+ } else {
4803
+ if (!types$1[name]) throw new Error(`Types doesn't contain name = ${name}`);
4804
+ type = types$1[name];
4805
+ }
4806
+ const parsed = [];
4807
+ for (; i < args.length; i++) {
4808
+ const input = context.parse(args[i], i, ValueType);
4809
+ if (!input) return null;
4810
+ parsed.push(input);
4811
+ }
4812
+ return new Assertion(type, parsed, context.key);
4813
+ }
4814
+ evaluate(ctx) {
4815
+ for (let i = 0; i < this.args.length; i++) {
4816
+ const value = this.args[i].evaluate(ctx);
4817
+ if (!checkSubtype(this.type, typeOf(value))) return value;
4818
+ else if (i === this.args.length - 1) throw new RuntimeError(`Expected value to be of type ${typeToString(this.type)}, but found ${typeToString(typeOf(value))} instead.`, this.key);
4819
+ }
4820
+ throw new Error();
4821
+ }
4822
+ eachChild(fn) {
4823
+ this.args.forEach(fn);
4824
+ }
4825
+ outputDefined() {
4826
+ return this.args.every((arg) => arg.outputDefined());
4827
+ }
4828
+ };
4829
+
4830
+ //#region src/expression/definitions/coercion.ts
4831
+ const types = {
4832
+ "to-boolean": BooleanType,
4833
+ "to-color": ColorType,
4834
+ "to-number": NumberType,
4835
+ "to-string": StringType
4836
+ };
4837
+ /**
4838
+ * Special form for error-coalescing coercion expressions "to-number",
4839
+ * "to-color". Since these coercions can fail at runtime, they accept multiple
4840
+ * arguments, only evaluating one at a time until one succeeds.
4841
+ *
4842
+ * @private
4843
+ */
4844
+ var Coercion = class Coercion {
4845
+ constructor(type, args, key) {
4846
+ this.type = type;
4415
4847
  this.args = args;
4416
4848
  this.key = key;
4417
4849
  }
@@ -4474,203 +4906,47 @@ var Coercion = class Coercion {
4474
4906
  input = arg.evaluate(ctx);
4475
4907
  const val = ColorArray.parse(input);
4476
4908
  if (val) return val;
4477
- }
4478
- throw new RuntimeError(`Could not parse colorArray from value '${typeof input === "string" ? input : JSON.stringify(input)}'`, this.key);
4479
- }
4480
- case "variableAnchorOffsetCollection": {
4481
- let input;
4482
- for (const arg of this.args) {
4483
- input = arg.evaluate(ctx);
4484
- const coll = VariableAnchorOffsetCollection.parse(input);
4485
- if (coll) return coll;
4486
- }
4487
- throw new RuntimeError(`Could not parse variableAnchorOffsetCollection from value '${typeof input === "string" ? input : JSON.stringify(input)}'`, this.key);
4488
- }
4489
- case "number": {
4490
- let value = null;
4491
- for (const arg of this.args) {
4492
- value = arg.evaluate(ctx);
4493
- if (value === null) return 0;
4494
- const num = Number(value);
4495
- if (isNaN(num)) continue;
4496
- return num;
4497
- }
4498
- throw new RuntimeError(`Could not convert ${JSON.stringify(value)} to number.`, this.key);
4499
- }
4500
- case "formatted": return Formatted.fromString(valueToString(this.args[0].evaluate(ctx)));
4501
- case "resolvedImage": return ResolvedImage.fromString(valueToString(this.args[0].evaluate(ctx)));
4502
- case "projectionDefinition": return this.args[0].evaluate(ctx);
4503
- default: return valueToString(this.args[0].evaluate(ctx));
4504
- }
4505
- }
4506
- eachChild(fn) {
4507
- this.args.forEach(fn);
4508
- }
4509
- outputDefined() {
4510
- return this.args.every((arg) => arg.outputDefined());
4511
- }
4512
- };
4513
- //#endregion
4514
- //#region src/expression/evaluation_context.ts
4515
- const geometryTypes = [
4516
- "Unknown",
4517
- "Point",
4518
- "LineString",
4519
- "Polygon"
4520
- ];
4521
- var EvaluationContext = class {
4522
- constructor() {
4523
- this.globals = null;
4524
- this.feature = null;
4525
- this.featureState = null;
4526
- this.formattedSection = null;
4527
- this._parseColorCache = /* @__PURE__ */ new Map();
4528
- this.availableImages = null;
4529
- this.canonical = null;
4530
- }
4531
- id() {
4532
- return this.feature && "id" in this.feature ? this.feature.id : null;
4533
- }
4534
- geometryType() {
4535
- return this.feature ? typeof this.feature.type === "number" ? geometryTypes[this.feature.type] : this.feature.type : null;
4536
- }
4537
- geometry() {
4538
- return this.feature && "geometry" in this.feature ? this.feature.geometry : null;
4539
- }
4540
- canonicalID() {
4541
- return this.canonical;
4542
- }
4543
- properties() {
4544
- return this.feature && this.feature.properties || {};
4545
- }
4546
- parseColor(input) {
4547
- let cached = this._parseColorCache.get(input);
4548
- if (!cached) {
4549
- cached = Color$1.parse(input);
4550
- this._parseColorCache.set(input, cached);
4551
- }
4552
- return cached;
4553
- }
4554
- };
4555
- //#endregion
4556
- //#region src/expression/parsing_context.ts
4557
- /**
4558
- * State associated parsing at a given point in an expression tree.
4559
- * @private
4560
- */
4561
- var ParsingContext = class ParsingContext {
4562
- constructor(registry, isConstantFunc, path = [], expectedType, scope = new Scope(), errors = []) {
4563
- this.registry = registry;
4564
- this.path = path;
4565
- this.key = path.map((part) => `[${part}]`).join("");
4566
- this.scope = scope;
4567
- this.errors = errors;
4568
- this.expectedType = expectedType;
4569
- this._isConstant = isConstantFunc;
4570
- }
4571
- /**
4572
- * @param expr the JSON expression to parse
4573
- * @param index the optional argument index if this expression is an argument of a parent expression that's being parsed
4574
- * @param options
4575
- * @param options.omitTypeAnnotations set true to omit inferred type annotations. Caller beware: with this option set, the parsed expression's type will NOT satisfy `expectedType` if it would normally be wrapped in an inferred annotation.
4576
- * @private
4577
- */
4578
- parse(expr, index, expectedType, bindings, options = {}) {
4579
- if (index) return this.concat(index, expectedType, bindings)._parse(expr, options);
4580
- return this._parse(expr, options);
4581
- }
4582
- _parse(expr, options) {
4583
- if (expr === null || typeof expr === "string" || typeof expr === "boolean" || typeof expr === "number") expr = ["literal", expr];
4584
- const key = this.key;
4585
- function annotate(parsed, type, typeAnnotation) {
4586
- if (typeAnnotation === "assert") return new Assertion(type, [parsed], key);
4587
- else if (typeAnnotation === "coerce") return new Coercion(type, [parsed], key);
4588
- else return parsed;
4589
- }
4590
- if (Array.isArray(expr)) {
4591
- if (expr.length === 0) return this.error("Expected an array with at least one element. If you wanted a literal array, use [\"literal\", []].");
4592
- const op = expr[0];
4593
- if (typeof op !== "string") {
4594
- this.error(`Expression name must be a string, but found ${typeof op} instead. If you wanted a literal array, use ["literal", [...]].`, 0);
4595
- return null;
4909
+ }
4910
+ throw new RuntimeError(`Could not parse colorArray from value '${typeof input === "string" ? input : JSON.stringify(input)}'`, this.key);
4596
4911
  }
4597
- const Expr = this.registry[op];
4598
- if (Expr) {
4599
- let parsed = Expr.parse(expr, this);
4600
- if (!parsed) return null;
4601
- if (this.expectedType) {
4602
- const expected = this.expectedType;
4603
- const actual = parsed.type;
4604
- if ((expected.kind === "string" || expected.kind === "number" || expected.kind === "boolean" || expected.kind === "object" || expected.kind === "array") && actual.kind === "value") parsed = annotate(parsed, expected, options.typeAnnotation || "assert");
4605
- else if ("projectionDefinition" === expected.kind && ["string", "array"].includes(actual.kind) || [
4606
- "color",
4607
- "formatted",
4608
- "resolvedImage"
4609
- ].includes(expected.kind) && ["value", "string"].includes(actual.kind) || ["padding", "numberArray"].includes(expected.kind) && [
4610
- "value",
4611
- "number",
4612
- "array"
4613
- ].includes(actual.kind) || "colorArray" === expected.kind && [
4614
- "value",
4615
- "string",
4616
- "array"
4617
- ].includes(actual.kind) || "variableAnchorOffsetCollection" === expected.kind && ["value", "array"].includes(actual.kind)) parsed = annotate(parsed, expected, options.typeAnnotation || "coerce");
4618
- else if (this.checkSubtype(expected, actual)) return null;
4912
+ case "variableAnchorOffsetCollection": {
4913
+ let input;
4914
+ for (const arg of this.args) {
4915
+ input = arg.evaluate(ctx);
4916
+ const coll = VariableAnchorOffsetCollection.parse(input);
4917
+ if (coll) return coll;
4619
4918
  }
4620
- if (!(parsed instanceof Literal) && parsed.type.kind !== "resolvedImage" && this._isConstant(parsed)) {
4621
- const ec = new EvaluationContext();
4622
- try {
4623
- parsed = new Literal(parsed.type, parsed.evaluate(ec));
4624
- } catch (e) {
4625
- this.error(e.message);
4626
- return null;
4627
- }
4919
+ throw new RuntimeError(`Could not parse variableAnchorOffsetCollection from value '${typeof input === "string" ? input : JSON.stringify(input)}'`, this.key);
4920
+ }
4921
+ case "number": {
4922
+ let value = null;
4923
+ for (const arg of this.args) {
4924
+ value = arg.evaluate(ctx);
4925
+ if (value === null) return 0;
4926
+ const num = Number(value);
4927
+ if (isNaN(num)) continue;
4928
+ return num;
4628
4929
  }
4629
- return parsed;
4930
+ throw new RuntimeError(`Could not convert ${JSON.stringify(value)} to number.`, this.key);
4630
4931
  }
4631
- return this.error(`Unknown expression "${op}". If you wanted a literal array, use ["literal", [...]].`, 0);
4632
- } else if (typeof expr === "undefined") return this.error("'undefined' value invalid. Use null instead.");
4633
- else if (typeof expr === "object") return this.error("Bare objects invalid. Use [\"literal\", {...}] instead.");
4634
- else return this.error(`Expected an array, but found ${typeof expr} instead.`);
4635
- }
4636
- /**
4637
- * Returns a copy of this context suitable for parsing the subexpression at
4638
- * index `index`, optionally appending to 'let' binding map.
4639
- *
4640
- * Note that `errors` property, intended for collecting errors while
4641
- * parsing, is copied by reference rather than cloned.
4642
- * @private
4643
- */
4644
- concat(index, expectedType, bindings) {
4645
- const path = typeof index === "number" ? this.path.concat(index) : this.path;
4646
- const scope = bindings ? this.scope.concat(bindings) : this.scope;
4647
- return new ParsingContext(this.registry, this._isConstant, path, expectedType || null, scope, this.errors);
4932
+ case "formatted": return Formatted.fromString(valueToString(this.args[0].evaluate(ctx)));
4933
+ case "resolvedImage": return ResolvedImage.fromString(valueToString(this.args[0].evaluate(ctx)));
4934
+ case "projectionDefinition": {
4935
+ const input = this.args[0].evaluate(ctx);
4936
+ if (ProjectionDefinition.parse(input)) return input;
4937
+ throw new RuntimeError(`Could not parse projectionDefinition from value '${typeof input === "string" ? input : JSON.stringify(input)}'`, this.key);
4938
+ }
4939
+ default: return valueToString(this.args[0].evaluate(ctx));
4940
+ }
4648
4941
  }
4649
- /**
4650
- * Push a parsing (or type checking) error into the `this.errors`
4651
- * @param error The message
4652
- * @param keys Optionally specify the source of the error at a child
4653
- * of the current expression at `this.key`.
4654
- * @private
4655
- */
4656
- error(error, ...keys) {
4657
- const key = `${this.key}${keys.map((k) => `[${k}]`).join("")}`;
4658
- this.errors.push(new ExpressionParsingError(key, error));
4942
+ eachChild(fn) {
4943
+ this.args.forEach(fn);
4659
4944
  }
4660
- /**
4661
- * Returns null if `t` is a subtype of `expected`; otherwise returns an
4662
- * error message and also pushes it to `this.errors`.
4663
- * @param expected The expected type
4664
- * @param t The actual type
4665
- * @returns null if `t` is a subtype of `expected`; otherwise returns an error message
4666
- */
4667
- checkSubtype(expected, t) {
4668
- const error = checkSubtype(expected, t);
4669
- if (error) this.error(error);
4670
- return error;
4945
+ outputDefined() {
4946
+ return this.args.every((arg) => arg.outputDefined());
4671
4947
  }
4672
4948
  };
4673
- //#endregion
4949
+
4674
4950
  //#region src/expression/definitions/let.ts
4675
4951
  var Let = class Let {
4676
4952
  constructor(bindings, result) {
@@ -4704,7 +4980,7 @@ var Let = class Let {
4704
4980
  return this.result.outputDefined();
4705
4981
  }
4706
4982
  };
4707
- //#endregion
4983
+
4708
4984
  //#region src/expression/definitions/var.ts
4709
4985
  var Var = class Var {
4710
4986
  constructor(name, boundExpression) {
@@ -4726,7 +5002,7 @@ var Var = class Var {
4726
5002
  return false;
4727
5003
  }
4728
5004
  };
4729
- //#endregion
5005
+
4730
5006
  //#region src/expression/definitions/at.ts
4731
5007
  var At = class At {
4732
5008
  constructor(type, index, input, key) {
@@ -4759,7 +5035,7 @@ var At = class At {
4759
5035
  return false;
4760
5036
  }
4761
5037
  };
4762
- //#endregion
5038
+
4763
5039
  //#region src/expression/definitions/in.ts
4764
5040
  var In = class In {
4765
5041
  constructor(needle, haystack, key) {
@@ -4803,7 +5079,7 @@ var In = class In {
4803
5079
  return true;
4804
5080
  }
4805
5081
  };
4806
- //#endregion
5082
+
4807
5083
  //#region src/expression/definitions/index_of.ts
4808
5084
  var IndexOf = class IndexOf {
4809
5085
  constructor(needle, haystack, key, fromIndex) {
@@ -4858,7 +5134,7 @@ var IndexOf = class IndexOf {
4858
5134
  return false;
4859
5135
  }
4860
5136
  };
4861
- //#endregion
5137
+
4862
5138
  //#region src/expression/definitions/match.ts
4863
5139
  var Match = class Match {
4864
5140
  constructor(inputType, outputType, input, cases, outputs, otherwise) {
@@ -4917,7 +5193,7 @@ var Match = class Match {
4917
5193
  return this.outputs.every((out) => out.outputDefined()) && this.otherwise.outputDefined();
4918
5194
  }
4919
5195
  };
4920
- //#endregion
5196
+
4921
5197
  //#region src/expression/definitions/case.ts
4922
5198
  var Case = class Case {
4923
5199
  constructor(type, branches, otherwise) {
@@ -4959,324 +5235,51 @@ var Case = class Case {
4959
5235
  return this.branches.every(([_, out]) => out.outputDefined()) && this.otherwise.outputDefined();
4960
5236
  }
4961
5237
  };
4962
- //#endregion
5238
+
4963
5239
  //#region src/expression/definitions/slice.ts
4964
5240
  var Slice = class Slice {
4965
- constructor(type, input, beginIndex, key, endIndex) {
4966
- this.type = type;
4967
- this.input = input;
4968
- this.beginIndex = beginIndex;
4969
- this.key = key;
4970
- this.endIndex = endIndex;
4971
- }
4972
- static parse(args, context) {
4973
- if (args.length <= 2 || args.length >= 5) return context.error(`Expected 2 or 3 arguments, but found ${args.length - 1} instead.`);
4974
- const input = context.parse(args[1], 1, ValueType);
4975
- const beginIndex = context.parse(args[2], 2, NumberType);
4976
- if (!input || !beginIndex) return null;
4977
- if (!isValidType(input.type, [
4978
- array(ValueType),
4979
- StringType,
4980
- ValueType
4981
- ])) return context.error(`Expected first argument to be of type array or string, but found ${typeToString(input.type)} instead`);
4982
- if (args.length === 4) {
4983
- const endIndex = context.parse(args[3], 3, NumberType);
4984
- if (!endIndex) return null;
4985
- return new Slice(input.type, input, beginIndex, context.key, endIndex);
4986
- } else return new Slice(input.type, input, beginIndex, context.key);
4987
- }
4988
- evaluate(ctx) {
4989
- const input = this.input.evaluate(ctx);
4990
- const beginIndex = this.beginIndex.evaluate(ctx);
4991
- let endIndex;
4992
- if (this.endIndex) endIndex = this.endIndex.evaluate(ctx);
4993
- if (isValidNativeType(input, ["string"])) return [...input].slice(beginIndex, endIndex).join("");
4994
- else if (isValidNativeType(input, ["array"])) return input.slice(beginIndex, endIndex);
4995
- else throw new RuntimeError(`Expected first argument to be of type array or string, but found ${typeToString(typeOf(input))} instead.`, this.key);
4996
- }
4997
- eachChild(fn) {
4998
- fn(this.input);
4999
- fn(this.beginIndex);
5000
- if (this.endIndex) fn(this.endIndex);
5001
- }
5002
- outputDefined() {
5003
- return false;
5004
- }
5005
- };
5006
- //#endregion
5007
- //#region src/expression/stops.ts
5008
- /**
5009
- * Returns the index of the last stop <= input, or 0 if it doesn't exist.
5010
- * @private
5011
- */
5012
- function findStopLessThanOrEqualTo(stops, input, key) {
5013
- const lastIndex = stops.length - 1;
5014
- let lowerIndex = 0;
5015
- let upperIndex = lastIndex;
5016
- let currentIndex = 0;
5017
- let currentValue, nextValue;
5018
- while (lowerIndex <= upperIndex) {
5019
- currentIndex = Math.floor((lowerIndex + upperIndex) / 2);
5020
- currentValue = stops[currentIndex];
5021
- nextValue = stops[currentIndex + 1];
5022
- if (currentValue <= input) {
5023
- if (currentIndex === lastIndex || input < nextValue) return currentIndex;
5024
- lowerIndex = currentIndex + 1;
5025
- } else if (currentValue > input) upperIndex = currentIndex - 1;
5026
- else throw new RuntimeError("Input is not a number.", key);
5027
- }
5028
- return 0;
5029
- }
5030
- //#endregion
5031
- //#region src/expression/definitions/step.ts
5032
- var Step = class Step {
5033
- constructor(type, input, stops, key) {
5034
- this.type = type;
5035
- this.input = input;
5036
- this.key = key;
5037
- this.labels = [];
5038
- this.outputs = [];
5039
- for (const [label, expression] of stops) {
5040
- this.labels.push(label);
5041
- this.outputs.push(expression);
5042
- }
5043
- }
5044
- static parse(args, context) {
5045
- if (args.length - 1 < 4) return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`);
5046
- if ((args.length - 1) % 2 !== 0) return context.error("Expected an even number of arguments.");
5047
- const input = context.parse(args[1], 1, NumberType);
5048
- if (!input) return null;
5049
- const stops = [];
5050
- let outputType = null;
5051
- if (context.expectedType && context.expectedType.kind !== "value") outputType = context.expectedType;
5052
- for (let i = 1; i < args.length; i += 2) {
5053
- const label = i === 1 ? -Infinity : args[i];
5054
- const value = args[i + 1];
5055
- const labelKey = i;
5056
- const valueKey = i + 1;
5057
- if (typeof label !== "number") return context.error("Input/output pairs for \"step\" expressions must be defined using literal numeric values (not computed expressions) for the input values.", labelKey);
5058
- if (stops.length && stops[stops.length - 1][0] >= label) return context.error("Input/output pairs for \"step\" expressions must be arranged with input values in strictly ascending order.", labelKey);
5059
- const parsed = context.parse(value, valueKey, outputType);
5060
- if (!parsed) return null;
5061
- outputType = outputType || parsed.type;
5062
- stops.push([label, parsed]);
5063
- }
5064
- return new Step(outputType, input, stops, context.key);
5065
- }
5066
- evaluate(ctx) {
5067
- const labels = this.labels;
5068
- const outputs = this.outputs;
5069
- if (labels.length === 1) return outputs[0].evaluate(ctx);
5070
- const value = this.input.evaluate(ctx);
5071
- if (value <= labels[0]) return outputs[0].evaluate(ctx);
5072
- const stopCount = labels.length;
5073
- if (value >= labels[stopCount - 1]) return outputs[stopCount - 1].evaluate(ctx);
5074
- return outputs[findStopLessThanOrEqualTo(labels, value, this.key)].evaluate(ctx);
5075
- }
5076
- eachChild(fn) {
5077
- fn(this.input);
5078
- for (const expression of this.outputs) fn(expression);
5079
- }
5080
- outputDefined() {
5081
- return this.outputs.every((out) => out.outputDefined());
5082
- }
5083
- };
5084
- //#endregion
5085
- //#region node_modules/@mapbox/unitbezier/index.js
5086
- function unitBezier(p1x, p1y, p2x, p2y) {
5087
- const cx = 3 * p1x;
5088
- const bx = 3 * (p2x - p1x) - cx;
5089
- const ax = 1 - cx - bx;
5090
- const cy = 3 * p1y;
5091
- const by = 3 * (p2y - p1y) - cy;
5092
- const ay = 1 - cy - by;
5093
- return function solve(x, epsilon = 1e-6) {
5094
- if (x <= 0) return 0;
5095
- if (x >= 1) return 1;
5096
- let t = x;
5097
- for (let i = 0; i < 8; i++) {
5098
- const x2 = ((ax * t + bx) * t + cx) * t - x;
5099
- if (Math.abs(x2) < epsilon) return ((ay * t + by) * t + cy) * t;
5100
- const d2 = (3 * ax * t + 2 * bx) * t + cx;
5101
- if (Math.abs(d2) < 1e-6) break;
5102
- t -= x2 / d2;
5103
- }
5104
- let t0 = 0;
5105
- let t1 = 1;
5106
- t = x;
5107
- for (let i = 0; i < 20; i++) {
5108
- const x2 = ((ax * t + bx) * t + cx) * t;
5109
- if (Math.abs(x2 - x) < epsilon) break;
5110
- if (x > x2) t0 = t;
5111
- else t1 = t;
5112
- t = (t0 + t1) * .5;
5113
- }
5114
- return ((ay * t + by) * t + cy) * t;
5115
- };
5116
- }
5117
- //#endregion
5118
- //#region src/expression/definitions/interpolate.ts
5119
- var Interpolate = class Interpolate {
5120
- constructor(type, operator, interpolation, input, stops, key) {
5241
+ constructor(type, input, beginIndex, key, endIndex) {
5121
5242
  this.type = type;
5122
- this.operator = operator;
5123
- this.interpolation = interpolation;
5124
5243
  this.input = input;
5244
+ this.beginIndex = beginIndex;
5125
5245
  this.key = key;
5126
- this.labels = [];
5127
- this.outputs = [];
5128
- for (const [label, expression] of stops) {
5129
- this.labels.push(label);
5130
- this.outputs.push(expression);
5131
- }
5132
- }
5133
- static interpolationFactor(interpolation, input, lower, upper) {
5134
- let t = 0;
5135
- if (interpolation.name === "exponential") t = exponentialInterpolation(input, interpolation.base, lower, upper);
5136
- else if (interpolation.name === "linear") t = exponentialInterpolation(input, 1, lower, upper);
5137
- else if (interpolation.name === "cubic-bezier") {
5138
- const c = interpolation.controlPoints;
5139
- t = unitBezier(c[0], c[1], c[2], c[3])(exponentialInterpolation(input, 1, lower, upper));
5140
- }
5141
- return t;
5246
+ this.endIndex = endIndex;
5142
5247
  }
5143
5248
  static parse(args, context) {
5144
- let [operator, interpolation, input, ...rest] = args;
5145
- if (!Array.isArray(interpolation) || interpolation.length === 0) return context.error("Expected an interpolation type expression.", 1);
5146
- if (interpolation[0] === "linear") interpolation = { name: "linear" };
5147
- else if (interpolation[0] === "exponential") {
5148
- const base = interpolation[1];
5149
- if (typeof base !== "number") return context.error("Exponential interpolation requires a numeric base.", 1, 1);
5150
- interpolation = {
5151
- name: "exponential",
5152
- base
5153
- };
5154
- } else if (interpolation[0] === "cubic-bezier") {
5155
- const controlPoints = interpolation.slice(1);
5156
- if (controlPoints.length !== 4 || controlPoints.some((t) => typeof t !== "number" || t < 0 || t > 1)) return context.error("Cubic bezier interpolation requires four numeric arguments with values between 0 and 1.", 1);
5157
- interpolation = {
5158
- name: "cubic-bezier",
5159
- controlPoints
5160
- };
5161
- } else return context.error(`Unknown interpolation type ${String(interpolation[0])}`, 1, 0);
5162
- if (args.length - 1 < 4) return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`);
5163
- if ((args.length - 1) % 2 !== 0) return context.error("Expected an even number of arguments.");
5164
- input = context.parse(input, 2, NumberType);
5165
- if (!input) return null;
5166
- const stops = [];
5167
- let outputType = null;
5168
- if ((operator === "interpolate-hcl" || operator === "interpolate-lab") && context.expectedType != ColorArrayType) outputType = ColorType;
5169
- else if (context.expectedType && context.expectedType.kind !== "value") outputType = context.expectedType;
5170
- for (let i = 0; i < rest.length; i += 2) {
5171
- const label = rest[i];
5172
- const value = rest[i + 1];
5173
- const labelKey = i + 3;
5174
- const valueKey = i + 4;
5175
- if (typeof label !== "number") return context.error("Input/output pairs for \"interpolate\" expressions must be defined using literal numeric values (not computed expressions) for the input values.", labelKey);
5176
- if (stops.length && stops[stops.length - 1][0] >= label) return context.error("Input/output pairs for \"interpolate\" expressions must be arranged with input values in strictly ascending order.", labelKey);
5177
- const parsed = context.parse(value, valueKey, outputType);
5178
- if (!parsed) return null;
5179
- outputType = outputType || parsed.type;
5180
- stops.push([label, parsed]);
5181
- }
5182
- if (!verifyType(outputType, NumberType) && !verifyType(outputType, ProjectionDefinitionType) && !verifyType(outputType, ColorType) && !verifyType(outputType, PaddingType) && !verifyType(outputType, NumberArrayType) && !verifyType(outputType, ColorArrayType) && !verifyType(outputType, VariableAnchorOffsetCollectionType) && !verifyType(outputType, array(NumberType))) return context.error(`Type ${typeToString(outputType)} is not interpolatable.`);
5183
- return new Interpolate(outputType, operator, interpolation, input, stops, context.key);
5249
+ if (args.length <= 2 || args.length >= 5) return context.error(`Expected 2 or 3 arguments, but found ${args.length - 1} instead.`);
5250
+ const input = context.parse(args[1], 1, ValueType);
5251
+ const beginIndex = context.parse(args[2], 2, NumberType);
5252
+ if (!input || !beginIndex) return null;
5253
+ if (!isValidType(input.type, [
5254
+ array(ValueType),
5255
+ StringType,
5256
+ ValueType
5257
+ ])) return context.error(`Expected first argument to be of type array or string, but found ${typeToString(input.type)} instead`);
5258
+ if (args.length === 4) {
5259
+ const endIndex = context.parse(args[3], 3, NumberType);
5260
+ if (!endIndex) return null;
5261
+ return new Slice(input.type, input, beginIndex, context.key, endIndex);
5262
+ } else return new Slice(input.type, input, beginIndex, context.key);
5184
5263
  }
5185
5264
  evaluate(ctx) {
5186
- const labels = this.labels;
5187
- const outputs = this.outputs;
5188
- if (labels.length === 1) return outputs[0].evaluate(ctx);
5189
- const value = this.input.evaluate(ctx);
5190
- if (value <= labels[0]) return outputs[0].evaluate(ctx);
5191
- const stopCount = labels.length;
5192
- if (value >= labels[stopCount - 1]) return outputs[stopCount - 1].evaluate(ctx);
5193
- const index = findStopLessThanOrEqualTo(labels, value, this.key);
5194
- const lower = labels[index];
5195
- const upper = labels[index + 1];
5196
- const t = Interpolate.interpolationFactor(this.interpolation, value, lower, upper);
5197
- const outputLower = outputs[index].evaluate(ctx);
5198
- const outputUpper = outputs[index + 1].evaluate(ctx);
5199
- switch (this.operator) {
5200
- case "interpolate": switch (this.type.kind) {
5201
- case "number": return interpolateNumber(outputLower, outputUpper, t);
5202
- case "color": return Color$1.interpolate(outputLower, outputUpper, t);
5203
- case "padding": return Padding.interpolate(outputLower, outputUpper, t);
5204
- case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t);
5205
- case "numberArray": return NumberArray.interpolate(outputLower, outputUpper, t);
5206
- case "variableAnchorOffsetCollection": return VariableAnchorOffsetCollection.interpolate(outputLower, outputUpper, t, this.key);
5207
- case "array": return interpolateArray(outputLower, outputUpper, t);
5208
- case "projectionDefinition": return ProjectionDefinition.interpolate(outputLower, outputUpper, t);
5209
- }
5210
- case "interpolate-hcl": switch (this.type.kind) {
5211
- case "color": return Color$1.interpolate(outputLower, outputUpper, t, "hcl");
5212
- case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t, "hcl");
5213
- }
5214
- case "interpolate-lab": switch (this.type.kind) {
5215
- case "color": return Color$1.interpolate(outputLower, outputUpper, t, "lab");
5216
- case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t, "lab");
5217
- }
5218
- }
5265
+ const input = this.input.evaluate(ctx);
5266
+ const beginIndex = this.beginIndex.evaluate(ctx);
5267
+ let endIndex;
5268
+ if (this.endIndex) endIndex = this.endIndex.evaluate(ctx);
5269
+ if (isValidNativeType(input, ["string"])) return [...input].slice(beginIndex, endIndex).join("");
5270
+ else if (isValidNativeType(input, ["array"])) return input.slice(beginIndex, endIndex);
5271
+ else throw new RuntimeError(`Expected first argument to be of type array or string, but found ${typeToString(typeOf(input))} instead.`, this.key);
5219
5272
  }
5220
5273
  eachChild(fn) {
5221
5274
  fn(this.input);
5222
- for (const expression of this.outputs) fn(expression);
5275
+ fn(this.beginIndex);
5276
+ if (this.endIndex) fn(this.endIndex);
5223
5277
  }
5224
5278
  outputDefined() {
5225
- return this.outputs.every((out) => out.outputDefined());
5279
+ return false;
5226
5280
  }
5227
5281
  };
5228
- /**
5229
- * Returns a ratio that can be used to interpolate between exponential function
5230
- * stops.
5231
- * How it works: Two consecutive stop values define a (scaled and shifted) exponential function `f(x) = a * base^x + b`, where `base` is the user-specified base,
5232
- * and `a` and `b` are constants affording sufficient degrees of freedom to fit
5233
- * the function to the given stops.
5234
- *
5235
- * Here's a bit of algebra that lets us compute `f(x)` directly from the stop
5236
- * values without explicitly solving for `a` and `b`:
5237
- *
5238
- * First stop value: `f(x0) = y0 = a * base^x0 + b`
5239
- * Second stop value: `f(x1) = y1 = a * base^x1 + b`
5240
- * => `y1 - y0 = a(base^x1 - base^x0)`
5241
- * => `a = (y1 - y0)/(base^x1 - base^x0)`
5242
- *
5243
- * Desired value: `f(x) = y = a * base^x + b`
5244
- * => `f(x) = y0 + a * (base^x - base^x0)`
5245
- *
5246
- * From the above, we can replace the `a` in `a * (base^x - base^x0)` and do a
5247
- * little algebra:
5248
- * ```
5249
- * a * (base^x - base^x0) = (y1 - y0)/(base^x1 - base^x0) * (base^x - base^x0)
5250
- * = (y1 - y0) * (base^x - base^x0) / (base^x1 - base^x0)
5251
- * ```
5252
- *
5253
- * If we let `(base^x - base^x0) / (base^x1 base^x0)`, then we have
5254
- * `f(x) = y0 + (y1 - y0) * ratio`. In other words, `ratio` may be treated as
5255
- * an interpolation factor between the two stops' output values.
5256
- *
5257
- * (Note: a slightly different form for `ratio`,
5258
- * `(base^(x-x0) - 1) / (base^(x1-x0) - 1) `, is equivalent, but requires fewer
5259
- * expensive `Math.pow()` operations.)
5260
- *
5261
- * @private
5262
- */
5263
- function exponentialInterpolation(input, base, lowerValue, upperValue) {
5264
- const difference = upperValue - lowerValue;
5265
- const progress = input - lowerValue;
5266
- if (difference === 0) return 0;
5267
- else if (base === 1) return progress / difference;
5268
- else return (Math.pow(base, progress) - 1) / (Math.pow(base, difference) - 1);
5269
- }
5270
- const interpolateFactory = {
5271
- color: Color$1.interpolate,
5272
- number: interpolateNumber,
5273
- padding: Padding.interpolate,
5274
- numberArray: NumberArray.interpolate,
5275
- colorArray: ColorArray.interpolate,
5276
- variableAnchorOffsetCollection: VariableAnchorOffsetCollection.interpolate,
5277
- array: interpolateArray
5278
- };
5279
- //#endregion
5282
+
5280
5283
  //#region src/expression/definitions/coalesce.ts
5281
5284
  var Coalesce = class Coalesce {
5282
5285
  constructor(type, args) {
@@ -5321,7 +5324,7 @@ var Coalesce = class Coalesce {
5321
5324
  return this.args.every((arg) => arg.outputDefined());
5322
5325
  }
5323
5326
  };
5324
- //#endregion
5327
+
5325
5328
  //#region src/expression/definitions/comparison.ts
5326
5329
  function isComparableType(op, type) {
5327
5330
  if (op === "==" || op === "!=") return type.kind === "boolean" || type.kind === "string" || type.kind === "number" || type.kind === "null" || type.kind === "value";
@@ -5444,7 +5447,7 @@ const LessThan = makeComparison("<", lt, ltCollate);
5444
5447
  const GreaterThan = makeComparison(">", gt, gtCollate);
5445
5448
  const LessThanOrEqual = makeComparison("<=", lteq, lteqCollate);
5446
5449
  const GreaterThanOrEqual = makeComparison(">=", gteq, gteqCollate);
5447
- //#endregion
5450
+
5448
5451
  //#region src/expression/definitions/collator.ts
5449
5452
  var CollatorExpression = class CollatorExpression {
5450
5453
  constructor(caseSensitive, diacriticSensitive, locale) {
@@ -5480,7 +5483,7 @@ var CollatorExpression = class CollatorExpression {
5480
5483
  return false;
5481
5484
  }
5482
5485
  };
5483
- //#endregion
5486
+
5484
5487
  //#region src/expression/definitions/number_format.ts
5485
5488
  var NumberFormat = class NumberFormat {
5486
5489
  constructor(number, locale, currency, unit, minFractionDigits, maxFractionDigits) {
@@ -5532,103 +5535,22 @@ var NumberFormat = class NumberFormat {
5532
5535
  currency: this.currency ? this.currency.evaluate(ctx) : void 0,
5533
5536
  unit: this.unit ? this.unit.evaluate(ctx) : void 0,
5534
5537
  minimumFractionDigits: this.minFractionDigits ? this.minFractionDigits.evaluate(ctx) : void 0,
5535
- maximumFractionDigits: this.maxFractionDigits ? this.maxFractionDigits.evaluate(ctx) : void 0
5536
- }).format(this.number.evaluate(ctx));
5537
- }
5538
- eachChild(fn) {
5539
- fn(this.number);
5540
- if (this.locale) fn(this.locale);
5541
- if (this.currency) fn(this.currency);
5542
- if (this.unit) fn(this.unit);
5543
- if (this.minFractionDigits) fn(this.minFractionDigits);
5544
- if (this.maxFractionDigits) fn(this.maxFractionDigits);
5545
- }
5546
- outputDefined() {
5547
- return false;
5548
- }
5549
- };
5550
- //#endregion
5551
- //#region src/expression/definitions/format.ts
5552
- var FormatExpression = class FormatExpression {
5553
- constructor(sections) {
5554
- this.type = FormattedType;
5555
- this.sections = sections;
5556
- }
5557
- static parse(args, context) {
5558
- if (args.length < 2) return context.error("Expected at least one argument.");
5559
- const firstArg = args[1];
5560
- if (!Array.isArray(firstArg) && typeof firstArg === "object") return context.error("First argument must be an image or text section.");
5561
- const sections = [];
5562
- let nextTokenMayBeObject = false;
5563
- for (let i = 1; i <= args.length - 1; ++i) {
5564
- const arg = args[i];
5565
- if (nextTokenMayBeObject && typeof arg === "object" && !Array.isArray(arg)) {
5566
- nextTokenMayBeObject = false;
5567
- let scale = null;
5568
- if (arg["font-scale"]) {
5569
- scale = context.parse(arg["font-scale"], 1, NumberType);
5570
- if (!scale) return null;
5571
- }
5572
- let font = null;
5573
- if (arg["text-font"]) {
5574
- font = context.parse(arg["text-font"], 1, array(StringType));
5575
- if (!font) return null;
5576
- }
5577
- let textColor = null;
5578
- if (arg["text-color"]) {
5579
- textColor = context.parse(arg["text-color"], 1, ColorType);
5580
- if (!textColor) return null;
5581
- }
5582
- let verticalAlign = null;
5583
- if (arg["vertical-align"]) {
5584
- if (typeof arg["vertical-align"] === "string" && !VERTICAL_ALIGN_OPTIONS.includes(arg["vertical-align"])) return context.error(`'vertical-align' must be one of: 'bottom', 'center', 'top' but found '${arg["vertical-align"]}' instead.`);
5585
- verticalAlign = context.parse(arg["vertical-align"], 1, StringType);
5586
- if (!verticalAlign) return null;
5587
- }
5588
- const lastExpression = sections[sections.length - 1];
5589
- lastExpression.scale = scale;
5590
- lastExpression.font = font;
5591
- lastExpression.textColor = textColor;
5592
- lastExpression.verticalAlign = verticalAlign;
5593
- } else {
5594
- const content = context.parse(args[i], 1, ValueType);
5595
- if (!content) return null;
5596
- const kind = content.type.kind;
5597
- if (kind !== "string" && kind !== "value" && kind !== "null" && kind !== "resolvedImage") return context.error("Formatted text type must be 'string', 'value', 'image' or 'null'.");
5598
- nextTokenMayBeObject = true;
5599
- sections.push({
5600
- content,
5601
- scale: null,
5602
- font: null,
5603
- textColor: null,
5604
- verticalAlign: null
5605
- });
5606
- }
5607
- }
5608
- return new FormatExpression(sections);
5609
- }
5610
- evaluate(ctx) {
5611
- const evaluateSection = (section) => {
5612
- const evaluatedContent = section.content.evaluate(ctx);
5613
- if (typeOf(evaluatedContent) === ResolvedImageType) return new FormattedSection("", evaluatedContent, null, null, null, section.verticalAlign ? section.verticalAlign.evaluate(ctx) : null);
5614
- return new FormattedSection(valueToString(evaluatedContent), null, section.scale ? section.scale.evaluate(ctx) : null, section.font ? section.font.evaluate(ctx).join(",") : null, section.textColor ? section.textColor.evaluate(ctx) : null, section.verticalAlign ? section.verticalAlign.evaluate(ctx) : null);
5615
- };
5616
- return new Formatted(this.sections.map(evaluateSection));
5538
+ maximumFractionDigits: this.maxFractionDigits ? this.maxFractionDigits.evaluate(ctx) : void 0
5539
+ }).format(this.number.evaluate(ctx));
5617
5540
  }
5618
5541
  eachChild(fn) {
5619
- for (const section of this.sections) {
5620
- fn(section.content);
5621
- if (section.scale) fn(section.scale);
5622
- if (section.font) fn(section.font);
5623
- if (section.textColor) fn(section.textColor);
5624
- if (section.verticalAlign) fn(section.verticalAlign);
5625
- }
5542
+ fn(this.number);
5543
+ if (this.locale) fn(this.locale);
5544
+ if (this.currency) fn(this.currency);
5545
+ if (this.unit) fn(this.unit);
5546
+ if (this.minFractionDigits) fn(this.minFractionDigits);
5547
+ if (this.maxFractionDigits) fn(this.maxFractionDigits);
5626
5548
  }
5627
5549
  outputDefined() {
5628
5550
  return false;
5629
5551
  }
5630
5552
  };
5631
- //#endregion
5553
+
5632
5554
  //#region src/expression/definitions/image.ts
5633
5555
  var ImageExpression = class ImageExpression {
5634
5556
  constructor(input) {
@@ -5654,7 +5576,7 @@ var ImageExpression = class ImageExpression {
5654
5576
  return false;
5655
5577
  }
5656
5578
  };
5657
- //#endregion
5579
+
5658
5580
  //#region src/expression/definitions/length.ts
5659
5581
  var Length = class Length {
5660
5582
  constructor(input, key) {
@@ -5682,7 +5604,7 @@ var Length = class Length {
5682
5604
  return false;
5683
5605
  }
5684
5606
  };
5685
- //#endregion
5607
+
5686
5608
  //#region src/util/geometry_util.ts
5687
5609
  const EXTENT = 8192;
5688
5610
  function getTileCoordinates(p, canonical) {
@@ -5778,7 +5700,7 @@ function twoSided(p1, p2, q1, q2) {
5778
5700
  if (det1 > 0 && det2 < 0 || det1 < 0 && det2 > 0) return true;
5779
5701
  return false;
5780
5702
  }
5781
- //#endregion
5703
+
5782
5704
  //#region src/expression/definitions/within.ts
5783
5705
  function getTilePolygon(coordinates, bbox, canonical) {
5784
5706
  const polygon = [];
@@ -5940,7 +5862,7 @@ var Within = class Within {
5940
5862
  return true;
5941
5863
  }
5942
5864
  };
5943
- //#endregion
5865
+
5944
5866
  //#region node_modules/tinyqueue/index.js
5945
5867
  var TinyQueue = class {
5946
5868
  constructor(data = [], compare = (a, b) => a < b ? -1 : a > b ? 1 : 0) {
@@ -5993,54 +5915,10 @@ var TinyQueue = class {
5993
5915
  data[pos] = item;
5994
5916
  }
5995
5917
  };
5996
- //#endregion
5997
- //#region src/util/classify_rings.ts
5998
- /**
5999
- * Classifies an array of rings into polygons with outer rings and holes
6000
- * @param rings - the rings to classify
6001
- * @param maxRings - the maximum number of rings to include in a polygon, use 0 to include all rings
6002
- * @returns an array of polygons with internal rings as holes
6003
- */
6004
- function classifyRings$1(rings, maxRings) {
6005
- if (rings.length <= 1) return [rings];
6006
- const polygons = [];
6007
- let polygon;
6008
- let ccw;
6009
- for (const ring of rings) {
6010
- const area = calculateSignedArea(ring);
6011
- if (area === 0) continue;
6012
- ring.area = Math.abs(area);
6013
- if (ccw === void 0) ccw = area < 0;
6014
- if (ccw === area < 0) {
6015
- if (polygon) polygons.push(polygon);
6016
- polygon = [ring];
6017
- } else polygon.push(ring);
6018
- }
6019
- if (polygon) polygons.push(polygon);
6020
- return polygons;
6021
- }
6022
- /**
6023
- * Returns the signed area for the polygon ring. Positive areas are exterior rings and
6024
- * have a clockwise winding. Negative areas are interior rings and have a counter clockwise
6025
- * ordering.
6026
- *
6027
- * @param ring - Exterior or interior ring
6028
- * @returns Signed area
6029
- */
6030
- function calculateSignedArea(ring) {
6031
- let sum = 0;
6032
- for (let i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) {
6033
- p1 = ring[i];
6034
- p2 = ring[j];
6035
- sum += (p2.x - p1.x) * (p1.y + p2.y);
6036
- }
6037
- return sum;
6038
- }
6039
- //#endregion
5918
+
6040
5919
  //#region src/util/cheap_ruler.ts
6041
5920
  const RE = 6378.137;
6042
- const FE = 1 / 298.257223563;
6043
- const E2 = FE * (2 - FE);
5921
+ const E2 = .0066943799901413165;
6044
5922
  const RAD = Math.PI / 180;
6045
5923
  var CheapRuler = class {
6046
5924
  constructor(lat) {
@@ -6049,7 +5927,7 @@ var CheapRuler = class {
6049
5927
  const w2 = 1 / (1 - E2 * (1 - coslat * coslat));
6050
5928
  const w = Math.sqrt(w2);
6051
5929
  this.kx = m * w * coslat;
6052
- this.ky = m * w * w2 * (1 - E2);
5930
+ this.ky = m * w * w2 * .9933056200098587;
6053
5931
  }
6054
5932
  /**
6055
5933
  * Given two points of the form [longitude, latitude], returns the distance.
@@ -6120,7 +5998,7 @@ var CheapRuler = class {
6120
5998
  return deg;
6121
5999
  }
6122
6000
  };
6123
- //#endregion
6001
+
6124
6002
  //#region src/expression/definitions/distance.ts
6125
6003
  const MinPointsSize = 100;
6126
6004
  const MinLinePointsSize = 50;
@@ -6388,9 +6266,7 @@ function pointToGeometryDistance(ctx, geometries) {
6388
6266
  case "LineString":
6389
6267
  dist = Math.min(dist, pointSetToPointSetDistance(pointPosition, false, geometry.coordinates, true, ruler, dist));
6390
6268
  break;
6391
- case "Polygon":
6392
- dist = Math.min(dist, pointsToPolygonDistance(pointPosition, false, geometry.coordinates, ruler, dist));
6393
- break;
6269
+ case "Polygon": dist = Math.min(dist, pointsToPolygonDistance(pointPosition, false, geometry.coordinates, ruler, dist));
6394
6270
  }
6395
6271
  if (dist === 0) return dist;
6396
6272
  }
@@ -6410,9 +6286,7 @@ function lineStringToGeometryDistance(ctx, geometries) {
6410
6286
  case "LineString":
6411
6287
  dist = Math.min(dist, pointSetToPointSetDistance(linePositions, true, geometry.coordinates, true, ruler, dist));
6412
6288
  break;
6413
- case "Polygon":
6414
- dist = Math.min(dist, pointsToPolygonDistance(linePositions, true, geometry.coordinates, ruler, dist));
6415
- break;
6289
+ case "Polygon": dist = Math.min(dist, pointsToPolygonDistance(linePositions, true, geometry.coordinates, ruler, dist));
6416
6290
  }
6417
6291
  if (dist === 0) return dist;
6418
6292
  }
@@ -6436,9 +6310,7 @@ function polygonToGeometryDistance(ctx, geometries) {
6436
6310
  case "LineString":
6437
6311
  dist = Math.min(dist, pointsToPolygonDistance(geometry.coordinates, true, polygon, ruler, dist));
6438
6312
  break;
6439
- case "Polygon":
6440
- dist = Math.min(dist, polygonToPolygonDistance(polygon, geometry.coordinates, ruler, dist));
6441
- break;
6313
+ case "Polygon": dist = Math.min(dist, polygonToPolygonDistance(polygon, geometry.coordinates, ruler, dist));
6442
6314
  }
6443
6315
  if (dist === 0) return dist;
6444
6316
  }
@@ -6494,7 +6366,48 @@ var Distance = class Distance {
6494
6366
  return true;
6495
6367
  }
6496
6368
  };
6497
- //#endregion
6369
+
6370
+ //#region src/expression/definitions/semiliteral.ts
6371
+ var Semiliteral = class Semiliteral {
6372
+ constructor(arr) {
6373
+ let elementType = null;
6374
+ for (const expr of arr) if (!elementType) elementType = expr.type;
6375
+ else if (elementType === expr.type) continue;
6376
+ else {
6377
+ elementType = ValueType;
6378
+ break;
6379
+ }
6380
+ this.type = array(elementType ?? ValueType, arr.length);
6381
+ this.arr = arr;
6382
+ }
6383
+ static parse(args, context) {
6384
+ if (args.length !== 2) return context.error(`'semiliteral' expression requires exactly one argument, but found ${args.length - 1} instead.`);
6385
+ if (!isValue(args[1])) return context.error(`invalid value of type "${typeof args[1]}"`);
6386
+ const value = args[1];
6387
+ const type = typeOf(value);
6388
+ if (type.kind === "array") {
6389
+ const arr = value;
6390
+ const arrayContext = context.concat(1);
6391
+ const parsed = [];
6392
+ for (let i = 0; i < arr.length; i++) {
6393
+ const item = arrayContext.parse(arr[i], i, ValueType);
6394
+ if (!item) return null;
6395
+ parsed.push(item);
6396
+ }
6397
+ return new Semiliteral(parsed);
6398
+ } else return new Literal(type, value);
6399
+ }
6400
+ evaluate(ctx) {
6401
+ return this.arr.map((arg) => arg.evaluate(ctx));
6402
+ }
6403
+ eachChild(fn) {
6404
+ this.arr.forEach(fn);
6405
+ }
6406
+ outputDefined() {
6407
+ return this.arr.every((arg) => arg.outputDefined());
6408
+ }
6409
+ };
6410
+
6498
6411
  //#region src/expression/definitions/global_state.ts
6499
6412
  var GlobalState = class GlobalState {
6500
6413
  constructor(key) {
@@ -6518,7 +6431,7 @@ var GlobalState = class GlobalState {
6518
6431
  return false;
6519
6432
  }
6520
6433
  };
6521
- //#endregion
6434
+
6522
6435
  //#region src/expression/definitions/index.ts
6523
6436
  const expressions = {
6524
6437
  "==": Equals,
@@ -6547,6 +6460,7 @@ const expressions = {
6547
6460
  number: Assertion,
6548
6461
  "number-format": NumberFormat,
6549
6462
  object: Assertion,
6463
+ semiliteral: Semiliteral,
6550
6464
  slice: Slice,
6551
6465
  step: Step,
6552
6466
  string: Assertion,
@@ -6559,7 +6473,163 @@ const expressions = {
6559
6473
  distance: Distance,
6560
6474
  "global-state": GlobalState
6561
6475
  };
6562
- //#endregion
6476
+
6477
+ //#region src/expression/parsing_error.ts
6478
+ var ExpressionParsingError = class extends Error {
6479
+ constructor(key, message) {
6480
+ super(message);
6481
+ this.message = message;
6482
+ this.key = key;
6483
+ }
6484
+ };
6485
+
6486
+ //#region src/expression/scope.ts
6487
+ /**
6488
+ * Tracks `let` bindings during expression parsing.
6489
+ * @private
6490
+ */
6491
+ var Scope = class Scope {
6492
+ constructor(parent, bindings = []) {
6493
+ this.parent = parent;
6494
+ this.bindings = {};
6495
+ for (const [name, expression] of bindings) this.bindings[name] = expression;
6496
+ }
6497
+ concat(bindings) {
6498
+ return new Scope(this, bindings);
6499
+ }
6500
+ get(name) {
6501
+ if (this.bindings[name]) return this.bindings[name];
6502
+ if (this.parent) return this.parent.get(name);
6503
+ throw new Error(`${name} not found in scope.`);
6504
+ }
6505
+ has(name) {
6506
+ if (this.bindings[name]) return true;
6507
+ return this.parent ? this.parent.has(name) : false;
6508
+ }
6509
+ };
6510
+
6511
+ //#region src/expression/parsing_context.ts
6512
+ /**
6513
+ * State associated parsing at a given point in an expression tree.
6514
+ * @private
6515
+ */
6516
+ var ParsingContext = class ParsingContext {
6517
+ constructor(registry, isConstantFunc, path = [], expectedType, scope = new Scope(), errors = []) {
6518
+ this.registry = registry;
6519
+ this.path = path;
6520
+ this.key = path.map((part) => `[${part}]`).join("");
6521
+ this.scope = scope;
6522
+ this.errors = errors;
6523
+ this.expectedType = expectedType;
6524
+ this._isConstant = isConstantFunc;
6525
+ }
6526
+ /**
6527
+ * @param expr the JSON expression to parse
6528
+ * @param index the optional argument index if this expression is an argument of a parent expression that's being parsed
6529
+ * @param options
6530
+ * @param options.omitTypeAnnotations set true to omit inferred type annotations. Caller beware: with this option set, the parsed expression's type will NOT satisfy `expectedType` if it would normally be wrapped in an inferred annotation.
6531
+ * @private
6532
+ */
6533
+ parse(expr, index, expectedType, bindings, options = {}) {
6534
+ if (index != null) return this.concat(index, expectedType, bindings)._parse(expr, options);
6535
+ return this._parse(expr, options);
6536
+ }
6537
+ _parse(expr, options) {
6538
+ if (expr === null || typeof expr === "string" || typeof expr === "boolean" || typeof expr === "number") expr = ["literal", expr];
6539
+ const key = this.key;
6540
+ function annotate(parsed, type, typeAnnotation) {
6541
+ if (typeAnnotation === "assert") return new Assertion(type, [parsed], key);
6542
+ else if (typeAnnotation === "coerce") return new Coercion(type, [parsed], key);
6543
+ else return parsed;
6544
+ }
6545
+ if (Array.isArray(expr)) {
6546
+ if (expr.length === 0) return this.error("Expected an array with at least one element. If you wanted a literal array, use [\"literal\", []].");
6547
+ const op = expr[0];
6548
+ if (typeof op !== "string") {
6549
+ this.error(`Expression name must be a string, but found ${typeof op} instead. If you wanted a literal array, use ["literal", [...]].`, 0);
6550
+ return null;
6551
+ }
6552
+ const Expr = this.registry[op];
6553
+ if (Expr) {
6554
+ let parsed = Expr.parse(expr, this);
6555
+ if (!parsed) return null;
6556
+ if (this.expectedType) {
6557
+ const expected = this.expectedType;
6558
+ const actual = parsed.type;
6559
+ if ((expected.kind === "string" || expected.kind === "number" || expected.kind === "boolean" || expected.kind === "object" || expected.kind === "array") && actual.kind === "value") parsed = annotate(parsed, expected, options.typeAnnotation || "assert");
6560
+ else if ("projectionDefinition" === expected.kind && [
6561
+ "string",
6562
+ "array",
6563
+ "value"
6564
+ ].includes(actual.kind) || [
6565
+ "color",
6566
+ "formatted",
6567
+ "resolvedImage"
6568
+ ].includes(expected.kind) && ["value", "string"].includes(actual.kind) || ["padding", "numberArray"].includes(expected.kind) && [
6569
+ "value",
6570
+ "number",
6571
+ "array"
6572
+ ].includes(actual.kind) || "colorArray" === expected.kind && [
6573
+ "value",
6574
+ "string",
6575
+ "array"
6576
+ ].includes(actual.kind) || "variableAnchorOffsetCollection" === expected.kind && ["value", "array"].includes(actual.kind)) parsed = annotate(parsed, expected, options.typeAnnotation || "coerce");
6577
+ else if (this.checkSubtype(expected, actual)) return null;
6578
+ }
6579
+ if (!(parsed instanceof Literal) && parsed.type.kind !== "resolvedImage" && this._isConstant(parsed)) {
6580
+ const ec = new EvaluationContext();
6581
+ try {
6582
+ parsed = new Literal(parsed.type, parsed.evaluate(ec));
6583
+ } catch (e) {
6584
+ this.error(e.message);
6585
+ return null;
6586
+ }
6587
+ }
6588
+ return parsed;
6589
+ }
6590
+ return this.error(`Unknown expression "${op}". If you wanted a literal array, use ["literal", [...]].`, 0);
6591
+ } else if (typeof expr === "undefined") return this.error("'undefined' value invalid. Use null instead.");
6592
+ else if (typeof expr === "object") return this.error("Bare objects invalid. Use [\"literal\", {...}] instead.");
6593
+ else return this.error(`Expected an array, but found ${typeof expr} instead.`);
6594
+ }
6595
+ /**
6596
+ * Returns a copy of this context suitable for parsing the subexpression at
6597
+ * index `index`, optionally appending to 'let' binding map.
6598
+ *
6599
+ * Note that `errors` property, intended for collecting errors while
6600
+ * parsing, is copied by reference rather than cloned.
6601
+ * @private
6602
+ */
6603
+ concat(index, expectedType, bindings) {
6604
+ const path = typeof index === "number" ? this.path.concat(index) : this.path;
6605
+ const scope = bindings ? this.scope.concat(bindings) : this.scope;
6606
+ return new ParsingContext(this.registry, this._isConstant, path, expectedType || null, scope, this.errors);
6607
+ }
6608
+ /**
6609
+ * Push a parsing (or type checking) error into the `this.errors`
6610
+ * @param error The message
6611
+ * @param keys Optionally specify the source of the error at a child
6612
+ * of the current expression at `this.key`.
6613
+ * @private
6614
+ */
6615
+ error(error, ...keys) {
6616
+ const key = `${this.key}${keys.map((k) => `[${k}]`).join("")}`;
6617
+ this.errors.push(new ExpressionParsingError(key, error));
6618
+ }
6619
+ /**
6620
+ * Returns null if `t` is a subtype of `expected`; otherwise returns an
6621
+ * error message and also pushes it to `this.errors`.
6622
+ * @param expected The expected type
6623
+ * @param t The actual type
6624
+ * @returns null if `t` is a subtype of `expected`; otherwise returns an error message
6625
+ */
6626
+ checkSubtype(expected, t) {
6627
+ const error = checkSubtype(expected, t);
6628
+ if (error) this.error(error);
6629
+ return error;
6630
+ }
6631
+ };
6632
+
6563
6633
  //#region src/expression/compound_expression.ts
6564
6634
  var CompoundExpression = class CompoundExpression {
6565
6635
  constructor(name, type, evaluate, args, key) {
@@ -7127,28 +7197,14 @@ function isStateConstant(e) {
7127
7197
  return result;
7128
7198
  }
7129
7199
  function isGlobalPropertyConstant(e, properties) {
7130
- if (e instanceof CompoundExpression && properties.indexOf(e.name) >= 0) return false;
7131
- let result = true;
7132
- e.eachChild((arg) => {
7133
- if (result && !isGlobalPropertyConstant(arg, properties)) result = false;
7134
- });
7135
- return result;
7136
- }
7137
- //#endregion
7138
- //#region src/util/result.ts
7139
- function success(value) {
7140
- return {
7141
- result: "success",
7142
- value
7143
- };
7144
- }
7145
- function error(value) {
7146
- return {
7147
- result: "error",
7148
- value
7149
- };
7200
+ if (e instanceof CompoundExpression && properties.indexOf(e.name) >= 0) return false;
7201
+ let result = true;
7202
+ e.eachChild((arg) => {
7203
+ if (result && !isGlobalPropertyConstant(arg, properties)) result = false;
7204
+ });
7205
+ return result;
7150
7206
  }
7151
- //#endregion
7207
+
7152
7208
  //#region src/util/properties.ts
7153
7209
  function supportsPropertyExpression(spec) {
7154
7210
  return spec["property-type"] === "data-driven" || spec["property-type"] === "cross-faded-data-driven";
@@ -7159,13 +7215,13 @@ function supportsZoomExpression(spec) {
7159
7215
  function supportsInterpolation(spec) {
7160
7216
  return !!spec.expression && spec.expression.interpolated;
7161
7217
  }
7162
- //#endregion
7218
+
7163
7219
  //#region src/util/extend.ts
7164
7220
  function extendBy(output, ...inputs) {
7165
7221
  for (const input of inputs) for (const k in input) output[k] = input[k];
7166
7222
  return output;
7167
7223
  }
7168
- //#endregion
7224
+
7169
7225
  //#region src/util/get_type.ts
7170
7226
  function getType(val) {
7171
7227
  if (val instanceof Number) return "number";
@@ -7175,7 +7231,7 @@ function getType(val) {
7175
7231
  else if (val === null) return "null";
7176
7232
  else return typeof val;
7177
7233
  }
7178
- //#endregion
7234
+
7179
7235
  //#region src/function/index.ts
7180
7236
  function isFunction(value) {
7181
7237
  return typeof value === "object" && value !== null && !Array.isArray(value) && typeOf(value) === ObjectType;
@@ -7273,21 +7329,21 @@ function createFunction(parameters, propertySpec) {
7273
7329
  kind: "source",
7274
7330
  evaluate(_, feature) {
7275
7331
  const value = feature && feature.properties ? feature.properties[parameters.property] : void 0;
7276
- if (value === void 0) return coalesce$1(parameters.default, propertySpec.default);
7332
+ if (value === void 0) return coalesce(parameters.default, propertySpec.default);
7277
7333
  return innerFun(parameters, propertySpec, value, hashedStops, categoricalKeyType);
7278
7334
  }
7279
7335
  };
7280
7336
  }
7281
- function coalesce$1(a, b, c) {
7337
+ function coalesce(a, b, c) {
7282
7338
  if (a !== void 0) return a;
7283
7339
  if (b !== void 0) return b;
7284
7340
  if (c !== void 0) return c;
7285
7341
  }
7286
7342
  function evaluateCategoricalFunction(parameters, propertySpec, input, hashedStops, keyType) {
7287
- return coalesce$1(typeof input === keyType ? hashedStops[input] : void 0, parameters.default, propertySpec.default);
7343
+ return coalesce(typeof input === keyType ? hashedStops[input] : void 0, parameters.default, propertySpec.default);
7288
7344
  }
7289
7345
  function evaluateIntervalFunction(parameters, propertySpec, input) {
7290
- if (getType(input) !== "number") return coalesce$1(parameters.default, propertySpec.default);
7346
+ if (getType(input) !== "number") return coalesce(parameters.default, propertySpec.default);
7291
7347
  const n = parameters.stops.length;
7292
7348
  if (n === 1) return parameters.stops[0][1];
7293
7349
  if (input <= parameters.stops[0][0]) return parameters.stops[0][1];
@@ -7297,13 +7353,13 @@ function evaluateIntervalFunction(parameters, propertySpec, input) {
7297
7353
  }
7298
7354
  function evaluateExponentialFunction(parameters, propertySpec, input) {
7299
7355
  const base = parameters.base !== void 0 ? parameters.base : 1;
7300
- if (getType(input) !== "number") return coalesce$1(parameters.default, propertySpec.default);
7356
+ if (getType(input) !== "number") return coalesce(parameters.default, propertySpec.default);
7301
7357
  const n = parameters.stops.length;
7302
7358
  if (n === 1) return parameters.stops[0][1];
7303
7359
  if (input <= parameters.stops[0][0]) return parameters.stops[0][1];
7304
7360
  if (input >= parameters.stops[n - 1][0]) return parameters.stops[n - 1][1];
7305
7361
  const index = findStopLessThanOrEqualTo(parameters.stops.map((stop) => stop[0]), input, "");
7306
- const t = interpolationFactor(input, base, parameters.stops[index][0], parameters.stops[index + 1][0]);
7362
+ const t = interpolationFactor$1(input, base, parameters.stops[index][0], parameters.stops[index + 1][0]);
7307
7363
  const outputLower = parameters.stops[index][1];
7308
7364
  const outputUpper = parameters.stops[index + 1][1];
7309
7365
  const interp = interpolateFactory[propertySpec.type] || identityFunction;
@@ -7337,7 +7393,7 @@ function evaluateIdentityFunction(parameters, propertySpec, input) {
7337
7393
  break;
7338
7394
  default: if (getType(input) !== propertySpec.type && (propertySpec.type !== "enum" || !propertySpec.values[input])) input = void 0;
7339
7395
  }
7340
- return coalesce$1(input, parameters.default, propertySpec.default);
7396
+ return coalesce(input, parameters.default, propertySpec.default);
7341
7397
  }
7342
7398
  /**
7343
7399
  * Returns a ratio that can be used to interpolate between exponential function
@@ -7377,14 +7433,28 @@ function evaluateIdentityFunction(parameters, propertySpec, input) {
7377
7433
  *
7378
7434
  * @private
7379
7435
  */
7380
- function interpolationFactor(input, base, lowerValue, upperValue) {
7436
+ function interpolationFactor$1(input, base, lowerValue, upperValue) {
7381
7437
  const difference = upperValue - lowerValue;
7382
7438
  const progress = input - lowerValue;
7383
7439
  if (difference === 0) return 0;
7384
7440
  else if (base === 1) return progress / difference;
7385
7441
  else return (Math.pow(base, progress) - 1) / (Math.pow(base, difference) - 1);
7386
7442
  }
7387
- //#endregion
7443
+
7444
+ //#region src/util/result.ts
7445
+ function success(value) {
7446
+ return {
7447
+ result: "success",
7448
+ value
7449
+ };
7450
+ }
7451
+ function error(value) {
7452
+ return {
7453
+ result: "error",
7454
+ value
7455
+ };
7456
+ }
7457
+
7388
7458
  //#region src/expression/index.ts
7389
7459
  var StyleExpression = class {
7390
7460
  constructor(expression, rootKey, propertySpec, globalState) {
@@ -7666,7 +7736,7 @@ function addGlobalState(globals, globalState) {
7666
7736
  globalState
7667
7737
  };
7668
7738
  }
7669
- //#endregion
7739
+
7670
7740
  //#region src/feature_filter/index.ts
7671
7741
  function classifyChildren(children) {
7672
7742
  let sawLegacy = false;
@@ -7772,12 +7842,10 @@ function findMixedLegacyFilter(filter, path = []) {
7772
7842
  if (diagnostic) return diagnostic;
7773
7843
  break;
7774
7844
  }
7775
- case "case":
7776
- for (let i = 1; i < filter.length - 1; i += 2) {
7777
- const diagnostic = checkChild(i, path, filter);
7778
- if (diagnostic) return diagnostic;
7779
- }
7780
- break;
7845
+ case "case": for (let i = 1; i < filter.length - 1; i += 2) {
7846
+ const diagnostic = checkChild(i, path, filter);
7847
+ if (diagnostic) return diagnostic;
7848
+ }
7781
7849
  }
7782
7850
  return null;
7783
7851
  }
@@ -7815,7 +7883,7 @@ function featureFilter(filter, rootKey, globalState) {
7815
7883
  needGeometry: false,
7816
7884
  getGlobalStateRefs: () => /* @__PURE__ */ new Set()
7817
7885
  };
7818
- if (!isExpressionFilter(filter)) filter = convertFilter$1(filter);
7886
+ if (!isExpressionFilter(filter)) filter = convertFilter(filter);
7819
7887
  else warnAboutMixedLegacyFilter(filter, rootKey);
7820
7888
  const compiled = createExpression(filter, rootKey, filterSpec, globalState);
7821
7889
  if (compiled.result === "error") throw new Error(compiled.value.map((err) => `${err.key}: ${err.message}`).join(", "));
@@ -7834,13 +7902,13 @@ function geometryNeeded(filter) {
7834
7902
  for (let index = 1; index < filter.length; index++) if (geometryNeeded(filter[index])) return true;
7835
7903
  return false;
7836
7904
  }
7837
- function convertFilter$1(filter) {
7905
+ function convertFilter(filter) {
7838
7906
  if (!filter) return true;
7839
7907
  const op = filter[0];
7840
7908
  if (filter.length <= 1) return op !== "any";
7841
- return op === "==" ? convertComparisonOp$1(filter[1], filter[2], "==") : op === "!=" ? convertNegation(convertComparisonOp$1(filter[1], filter[2], "==")) : op === "<" || op === ">" || op === "<=" || op === ">=" ? convertComparisonOp$1(filter[1], filter[2], op) : op === "any" ? convertDisjunctionOp(filter.slice(1)) : op === "all" ? ["all"].concat(filter.slice(1).map(convertFilter$1)) : op === "none" ? ["all"].concat(filter.slice(1).map(convertFilter$1).map(convertNegation)) : op === "in" ? convertInOp$1(filter[1], filter.slice(2)) : op === "!in" ? convertNegation(convertInOp$1(filter[1], filter.slice(2))) : op === "has" ? convertHasOp$1(filter[1]) : op === "!has" ? convertNegation(convertHasOp$1(filter[1])) : true;
7909
+ return op === "==" ? convertComparisonOp(filter[1], filter[2], "==") : op === "!=" ? convertNegation(convertComparisonOp(filter[1], filter[2], "==")) : op === "<" || op === ">" || op === "<=" || op === ">=" ? convertComparisonOp(filter[1], filter[2], op) : op === "any" ? convertDisjunctionOp(filter.slice(1)) : op === "all" ? ["all"].concat(filter.slice(1).map(convertFilter)) : op === "none" ? ["all"].concat(filter.slice(1).map(convertFilter).map(convertNegation)) : op === "in" ? convertInOp(filter[1], filter.slice(2)) : op === "!in" ? convertNegation(convertInOp(filter[1], filter.slice(2))) : op === "has" ? convertHasOp(filter[1]) : op === "!has" ? convertNegation(convertHasOp(filter[1])) : true;
7842
7910
  }
7843
- function convertComparisonOp$1(property, value, op) {
7911
+ function convertComparisonOp(property, value, op) {
7844
7912
  switch (property) {
7845
7913
  case "$type": return [`filter-type-${op}`, value];
7846
7914
  case "$id": return [`filter-id-${op}`, value];
@@ -7852,9 +7920,9 @@ function convertComparisonOp$1(property, value, op) {
7852
7920
  }
7853
7921
  }
7854
7922
  function convertDisjunctionOp(filters) {
7855
- return ["any"].concat(filters.map(convertFilter$1));
7923
+ return ["any"].concat(filters.map(convertFilter));
7856
7924
  }
7857
- function convertInOp$1(property, values) {
7925
+ function convertInOp(property, values) {
7858
7926
  if (values.length === 0) return false;
7859
7927
  switch (property) {
7860
7928
  case "$type": return ["filter-type-in", ["literal", values]];
@@ -7871,7 +7939,7 @@ function convertInOp$1(property, values) {
7871
7939
  ];
7872
7940
  }
7873
7941
  }
7874
- function convertHasOp$1(property) {
7942
+ function convertHasOp(property) {
7875
7943
  switch (property) {
7876
7944
  case "$type": return true;
7877
7945
  case "$id": return ["filter-has-id"];
@@ -8187,21 +8255,29 @@ class SVGRenderer {
8187
8255
  width;
8188
8256
  height;
8189
8257
  #svg;
8190
- #backgroundColor;
8191
8258
  #spriteSheetDefs = new Map();
8192
8259
  #spriteSymbolDefs = new Map();
8193
8260
  #sdfFilterDefs = new Map();
8194
8261
  #blurFilterDefs = new Map();
8262
+ #rasterDefs = [];
8263
+ #rasterClipCount = 0;
8264
+ #clipCircle;
8195
8265
  constructor(opt) {
8196
8266
  this.width = opt.width;
8197
8267
  this.height = opt.height;
8198
8268
  this.#svg = [];
8199
- this.#backgroundColor = Color.transparent;
8269
+ }
8270
+ setClipCircle(circle) {
8271
+ this.#clipCircle = circle;
8200
8272
  }
8201
8273
  drawBackgroundFill(style) {
8274
+ // Every background layer paints over what is below it, like any other layer. (A
8275
+ // transparent one, e.g. an empty "slot" layer, must not erase an earlier background.)
8202
8276
  const color = new Color(style.color);
8203
8277
  color.alpha *= style.opacity;
8204
- this.#backgroundColor = color;
8278
+ if (color.alpha <= 0)
8279
+ return;
8280
+ this.#svg.push(`<rect x="-1" y="-1" width="${(this.width + 2).toFixed(0)}" height="${(this.height + 2).toFixed(0)}" ${fillAttr(color)} />`);
8205
8281
  }
8206
8282
  drawPolygons(id, features) {
8207
8283
  if (features.length === 0)
@@ -8209,9 +8285,11 @@ class SVGRenderer {
8209
8285
  // Merge only *consecutive* features with identical attributes, so the paint
8210
8286
  // order of overlapping features matches MapLibre's source order. A global
8211
8287
  // merge would reorder interleaved features (e.g. data-driven fill colors).
8212
- // Fills and their antialias outlines are collected in one pass but emitted
8213
- // as two: MapLibre draws every fill first, then every outline, so a lower
8214
- // feature's outline composites on top of a later overlapping fill.
8288
+ // Translucent features are never merged: MapLibre blends each one separately,
8289
+ // so where they overlap, their opacity adds up — within one merged <path> it
8290
+ // would not. Fills and their antialias outlines are collected in one pass but
8291
+ // emitted as two: MapLibre draws every fill first, then every outline, so a
8292
+ // lower feature's outline composites on top of a later overlapping fill.
8215
8293
  const fillGroups = [];
8216
8294
  const outlineGroups = [];
8217
8295
  let currentFillKey;
@@ -8226,37 +8304,48 @@ class SVGRenderer {
8226
8304
  // Round each ring once; shared by the fill and the outline pass.
8227
8305
  const rings = feature.geometry.map((ring) => ring.map((p) => roundXY(p.x, p.y)));
8228
8306
  const color = new Color(style.color);
8307
+ const translucent = style.opacity < 1 || color.alpha < 255;
8229
8308
  if (color.alpha > 0) {
8230
8309
  const key = color.hex + translate + opacityAttr;
8231
- if (key !== currentFillKey) {
8310
+ if (translucent || key !== currentFillKey) {
8232
8311
  fillGroups.push({ segments: [], attrs: `${fillAttr(color)}${translate}${opacityAttr}` });
8233
- currentFillKey = key;
8312
+ currentFillKey = translucent ? undefined : key;
8234
8313
  }
8235
8314
  const group = fillGroups[fillGroups.length - 1];
8236
8315
  for (const ring of rings)
8237
8316
  group.segments.push(ring);
8238
8317
  }
8239
- // fill-antialias outline in an explicit fill-outline-color. MapLibre also
8240
- // draws a *default* outline in the fill color, but that outline IS its fill-
8241
- // edge antialiasing (draw_fill.ts clips it to just outside the shape) — and
8242
- // an SVG <path fill> is already rasterizer-antialiased, so redrawing it is
8243
- // redundant: measured identical pixel diff for +46–88% SVG size. We only
8244
- // draw the outline when a distinct color is set — the visible-border case
8245
- // (choropleths), which the browser's fill AA does not cover.
8246
- if (style.antialias && style.outlineColor !== undefined) {
8247
- const outlineColor = new Color(style.outlineColor);
8318
+ // fill-antialias outline, in fill-outline-color or else the fill color. For an
8319
+ // opaque fill, MapLibre's default outline IS its fill-edge antialiasing
8320
+ // (draw_fill.ts keeps it outside of the shape), and an SVG <path fill> is already
8321
+ // rasterizer-antialiased, so redrawing it is redundant: measured identical pixel
8322
+ // diff for +46–88% SVG size. It is drawn when it shows: in a distinct
8323
+ // fill-outline-color (choropleths), or for a translucent fill, where MapLibre
8324
+ // draws the outline over the fill's edge (e.g. buildings fading in).
8325
+ if (style.antialias && (style.outlineColor !== undefined || translucent)) {
8326
+ const outlineColor = style.outlineColor !== undefined ? new Color(style.outlineColor) : color;
8248
8327
  if (outlineColor.alpha > 0) {
8328
+ const outlineTranslucent = translucent || outlineColor.alpha < 255;
8249
8329
  const key = outlineColor.hex + translate + opacityAttr;
8250
- if (key !== currentOutlineKey) {
8330
+ if (outlineTranslucent || key !== currentOutlineKey) {
8251
8331
  outlineGroups.push({
8252
- segments: [],
8332
+ closed: [],
8333
+ open: [],
8253
8334
  attrs: `fill="none" ${strokeAttr(outlineColor, formatScaled(FILL_OUTLINE_WIDTH_PX))}${translate}${opacityAttr}`,
8254
8335
  });
8255
- currentOutlineKey = key;
8336
+ currentOutlineKey = outlineTranslucent ? undefined : key;
8256
8337
  }
8257
8338
  const group = outlineGroups[outlineGroups.length - 1];
8258
- for (const ring of rings)
8259
- group.segments.push(ring);
8339
+ // A polygon clipped to its tile has an outline without the clipped edges.
8340
+ if (feature.outline) {
8341
+ for (const line of feature.outline) {
8342
+ group.open.push(line.map((p) => roundXY(p.x, p.y)));
8343
+ }
8344
+ }
8345
+ else {
8346
+ for (const ring of rings)
8347
+ group.closed.push(ring);
8348
+ }
8260
8349
  }
8261
8350
  }
8262
8351
  });
@@ -8264,8 +8353,9 @@ class SVGRenderer {
8264
8353
  for (const { segments, attrs } of fillGroups) {
8265
8354
  this.#svg.push(`<path d="${segmentsToPath(segments, true)}" ${attrs} />`);
8266
8355
  }
8267
- for (const { segments, attrs } of outlineGroups) {
8268
- this.#svg.push(`<path d="${segmentsToPath(segments, true)}" ${attrs} />`);
8356
+ for (const { closed, open, attrs } of outlineGroups) {
8357
+ const d = segmentsToPath(closed, true) + segmentsToPath(open, false);
8358
+ this.#svg.push(`<path d="${d}" ${attrs} />`);
8269
8359
  }
8270
8360
  this.#svg.push('</g>');
8271
8361
  }
@@ -8273,7 +8363,12 @@ class SVGRenderer {
8273
8363
  if (features.length === 0)
8274
8364
  return;
8275
8365
  // Merge only *consecutive* same-attribute features (see drawPolygons) so the
8276
- // paint order of overlapping lines matches MapLibre's source order.
8366
+ // paint order of overlapping lines matches MapLibre's source order. Translucent
8367
+ // lines are never merged: MapLibre blends every line separately, so where lines
8368
+ // overlap (e.g. parallel rail tracks fading in), their opacity adds up — even
8369
+ // between the parts of one feature (tiles merge equal ways into one multi-line
8370
+ // feature). One <path> covers its overlaps only once, so a translucent line gets
8371
+ // a <path> per part.
8277
8372
  const groups = [];
8278
8373
  let currentKey;
8279
8374
  features.forEach(([feature, style]) => {
@@ -8308,7 +8403,8 @@ class SVGRenderer {
8308
8403
  translate,
8309
8404
  filterAttr,
8310
8405
  ].join('\0');
8311
- if (key !== currentKey) {
8406
+ const translucent = effectiveOpacity < 1 || color.alpha < 255;
8407
+ if (translucent || key !== currentKey) {
8312
8408
  const attrs = [
8313
8409
  'fill="none"',
8314
8410
  strokeAttr(color, roundedWidth),
@@ -8321,8 +8417,9 @@ class SVGRenderer {
8321
8417
  groups.push({
8322
8418
  segments: [],
8323
8419
  attrs: attrs.join(' ') + translate + opacityAttr + filterAttr,
8420
+ separate: translucent,
8324
8421
  });
8325
- currentKey = key;
8422
+ currentKey = translucent ? undefined : key;
8326
8423
  }
8327
8424
  const group = groups[groups.length - 1];
8328
8425
  feature.geometry.forEach((line) => {
@@ -8331,7 +8428,13 @@ class SVGRenderer {
8331
8428
  });
8332
8429
  });
8333
8430
  this.#svg.push(`<g id="${escapeXml(id)}">`);
8334
- for (const { segments, attrs } of groups) {
8431
+ for (const { segments, attrs, separate } of groups) {
8432
+ if (separate) {
8433
+ for (const segment of segments) {
8434
+ this.#svg.push(`<path d="${segmentsToPath([segment])}" ${attrs} />`);
8435
+ }
8436
+ continue;
8437
+ }
8335
8438
  const chains = chainSegments(segments);
8336
8439
  const d = segmentsToPath(chains);
8337
8440
  this.#svg.push(`<path d="${d}" ${attrs} />`);
@@ -8588,7 +8691,10 @@ class SVGRenderer {
8588
8691
  gAttrs += ` filter="${filters.join(' ')}"`;
8589
8692
  this.#svg.push(`<g ${gAttrs}>`);
8590
8693
  const pixelated = style.resampling === 'nearest';
8694
+ this.#drawRasterMeshes(tiles, pixelated);
8591
8695
  for (const tile of tiles) {
8696
+ if (tile.triangles)
8697
+ continue;
8592
8698
  const overlap = Math.min(tile.width, tile.height) / 10000; // slight overlap to prevent sub-pixel gaps between tiles
8593
8699
  let attrs = `x="${formatScaled(tile.x - overlap)}" y="${formatScaled(tile.y - overlap)}" width="${formatScaled(tile.width + overlap * 2)}" height="${formatScaled(tile.height + overlap * 2)}" xlink:href="${tile.dataUri}"`;
8594
8700
  if (pixelated)
@@ -8597,11 +8703,65 @@ class SVGRenderer {
8597
8703
  }
8598
8704
  this.#svg.push('</g>');
8599
8705
  }
8706
+ /**
8707
+ * Draws raster tiles given as meshes of triangles (globe projection). Every tile image is
8708
+ * defined once (as a square of RASTER_TILE_UNITS); every triangle shows it through the affine
8709
+ * transform that maps its three source corners exactly onto its three screen corners, clipped
8710
+ * to the triangle on screen.
8711
+ *
8712
+ * Seams: within a tile, each clip triangle is grown by RASTER_TRIANGLE_OVERLAP_PX; the same
8713
+ * transform continues there, so the overlap shows (almost) the same pixels as the neighbour.
8714
+ * At tile borders that is not enough, as both images end on the same line and their anti-
8715
+ * aliased edges let a hairline of the background shine through. So first, as an underlay,
8716
+ * the triangles along the tile borders are drawn with their image reaching beyond the border
8717
+ * (see {@link bleedAtTileBorder}); then all triangles are drawn exactly on top of it. The
8718
+ * slightly shifted underlay only shows through the seam.
8719
+ */
8720
+ #drawRasterMeshes(tiles, pixelated) {
8721
+ const meshes = [];
8722
+ for (const tile of tiles) {
8723
+ if (!tile.triangles)
8724
+ continue;
8725
+ const imageId = `raster-${String(this.#rasterDefs.length)}`;
8726
+ const size = String(RASTER_TILE_UNITS);
8727
+ let imageAttrs = `id="${imageId}" width="${size}" height="${size}" preserveAspectRatio="none" xlink:href="${tile.dataUri}"`;
8728
+ if (pixelated)
8729
+ imageAttrs += ' style="image-rendering:pixelated"';
8730
+ this.#rasterDefs.push(`<image ${imageAttrs} />`);
8731
+ meshes.push({ imageId, triangles: tile.triangles });
8732
+ }
8733
+ for (const { imageId, triangles } of meshes) {
8734
+ for (const { source, target } of triangles) {
8735
+ if (!isOnTileBorder(source))
8736
+ continue;
8737
+ this.#drawRasterTriangle(imageId, bleedAtTileBorder(source, target), target);
8738
+ }
8739
+ }
8740
+ for (const { imageId, triangles } of meshes) {
8741
+ for (const { source, target } of triangles) {
8742
+ this.#drawRasterTriangle(imageId, source, target);
8743
+ }
8744
+ }
8745
+ }
8746
+ #drawRasterTriangle(imageId, source, target) {
8747
+ const matrix = affineFromTriangles(source, target);
8748
+ if (!matrix)
8749
+ return;
8750
+ const clip = growTriangle(target, RASTER_TRIANGLE_OVERLAP_PX);
8751
+ const clipId = `raster-clip-${String(this.#rasterClipCount++)}`;
8752
+ const d = segmentsToPath([clip.map(([x, y]) => roundXY(x, y))], true);
8753
+ this.#svg.push(`<clipPath id="${clipId}"><path d="${d}"/></clipPath>` +
8754
+ `<g clip-path="url(#${clipId})"><use xlink:href="#${imageId}" transform="matrix(${matrix.map(formatUnit).join(',')})" /></g>`);
8755
+ }
8600
8756
  getString() {
8601
8757
  const w = this.width.toFixed(0);
8602
8758
  const h = this.height.toFixed(0);
8603
8759
  // Build defs content
8604
- const defsContent = [`<clipPath id="vb"><rect width="${w}" height="${h}"/></clipPath>`];
8760
+ const clip = this.#clipCircle;
8761
+ const clipShape = clip
8762
+ ? `<circle cx="${formatUnit(clip.x)}" cy="${formatUnit(clip.y)}" r="${formatUnit(clip.radius)}"/>`
8763
+ : `<rect width="${w}" height="${h}"/>`;
8764
+ const defsContent = [`<clipPath id="vb">${clipShape}</clipPath>`, ...this.#rasterDefs];
8605
8765
  for (const sheet of this.#spriteSheetDefs.values()) {
8606
8766
  defsContent.push(`<image id="${escapeXml(sheet.defId)}" width="${formatNum(sheet.width)}" height="${formatNum(sheet.height)}" xlink:href="${escapeXml(sheet.href)}" />`);
8607
8767
  }
@@ -8628,9 +8788,6 @@ class SVGRenderer {
8628
8788
  `<defs>\n ${defsContent.join('\n ')}\n</defs>`,
8629
8789
  `<g id="map" clip-path="url(#vb)">`,
8630
8790
  ];
8631
- if (this.#backgroundColor.alpha > 0) {
8632
- parts.push(`<rect x="-1" y="-1" width="${(this.width + 2).toFixed(0)}" height="${(this.height + 2).toFixed(0)}" ${fillAttr(this.#backgroundColor)} />`);
8633
- }
8634
8791
  parts.push(...this.#svg, '</g>', '</svg>');
8635
8792
  return parts.join('\n');
8636
8793
  }
@@ -8650,6 +8807,85 @@ function strokeAttr(color, width) {
8650
8807
  function formatScaled(v) {
8651
8808
  return formatNum(Math.round(v * 10));
8652
8809
  }
8810
+ /** Size of a raster tile image in user units, when drawn as a mesh of triangles on the globe. */
8811
+ const RASTER_TILE_UNITS = 256;
8812
+ /** How far (pixels) a raster tile image reaches beyond the tile border in the seam underlay. */
8813
+ const RASTER_TILE_BLEED_PX = 1;
8814
+ /** How far (pixels) each raster triangle is grown beyond its edges to overlap its neighbours. */
8815
+ const RASTER_TRIANGLE_OVERLAP_PX = 0.5;
8816
+ /**
8817
+ * The affine transform [a, b, c, d, e, f] mapping the source triangle (in tile units 0..1,
8818
+ * scaled to RASTER_TILE_UNITS) exactly onto the target triangle, or undefined if either is
8819
+ * degenerate.
8820
+ */
8821
+ function affineFromTriangles(source, target) {
8822
+ const k = RASTER_TILE_UNITS;
8823
+ const [[u0, v0], [u1, v1], [u2, v2]] = source.map(([u, v]) => [u * k, v * k]);
8824
+ const [[x0, y0], [x1, y1], [x2, y2]] = target;
8825
+ const du1 = u1 - u0;
8826
+ const dv1 = v1 - v0;
8827
+ const du2 = u2 - u0;
8828
+ const dv2 = v2 - v0;
8829
+ const det = du1 * dv2 - du2 * dv1;
8830
+ if (Math.abs(det) < 1e-12)
8831
+ return undefined;
8832
+ const dx1 = x1 - x0;
8833
+ const dx2 = x2 - x0;
8834
+ const dy1 = y1 - y0;
8835
+ const dy2 = y2 - y0;
8836
+ // Screen area below a hundredth of a pixel: nothing to draw.
8837
+ if (Math.abs(dx1 * dy2 - dx2 * dy1) < 0.02)
8838
+ return undefined;
8839
+ const a = (dx1 * dv2 - dx2 * dv1) / det;
8840
+ const c = (du1 * dx2 - du2 * dx1) / det;
8841
+ const b = (dy1 * dv2 - dy2 * dv1) / det;
8842
+ const d = (du1 * dy2 - du2 * dy1) / det;
8843
+ return [a, b, c, d, x0 - a * u0 - c * v0, y0 - b * u0 - d * v0];
8844
+ }
8845
+ function isOnTileBorder(source) {
8846
+ return source.some(([u, v]) => u === 0 || u === 1 || v === 0 || v === 1);
8847
+ }
8848
+ /**
8849
+ * Moves the source corners of a triangle that lie on the tile border inwards, by the
8850
+ * equivalent of RASTER_TILE_BLEED_PX on screen, so that the image edge lies beyond the border.
8851
+ * (Used for the underlay that closes the seams between tiles, see `#drawRasterMeshes`.)
8852
+ */
8853
+ function bleedAtTileBorder(source, target) {
8854
+ const [[u0, v0], [u1, v1], [u2, v2]] = source;
8855
+ const [[x0, y0], [x1, y1], [x2, y2]] = target;
8856
+ const sourceArea = Math.abs((u1 - u0) * (v2 - v0) - (u2 - u0) * (v1 - v0));
8857
+ const targetArea = Math.abs((x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0));
8858
+ if (sourceArea === 0 || targetArea === 0)
8859
+ return source;
8860
+ // Pixels per tile unit, and the bleed in tile units, kept within the triangle.
8861
+ const scale = Math.sqrt(targetArea / sourceArea);
8862
+ const size = Math.max(Math.abs(u1 - u0), Math.abs(u2 - u0), Math.abs(v1 - v0), Math.abs(v2 - v0));
8863
+ const bleed = Math.min(RASTER_TILE_BLEED_PX / scale, size / 4);
8864
+ const inset = (t) => (t === 0 ? bleed : t === 1 ? 1 - bleed : t);
8865
+ return source.map(([u, v]) => [inset(u), inset(v)]);
8866
+ }
8867
+ /**
8868
+ * Moves every edge of a triangle outwards by `distance`, by scaling it around its incenter.
8869
+ * Growth is limited for very thin triangles, whose corners would otherwise shoot far out.
8870
+ */
8871
+ function growTriangle(triangle, distance) {
8872
+ const [[x0, y0], [x1, y1], [x2, y2]] = triangle;
8873
+ const a = Math.hypot(x2 - x1, y2 - y1);
8874
+ const b = Math.hypot(x2 - x0, y2 - y0);
8875
+ const c = Math.hypot(x1 - x0, y1 - y0);
8876
+ const perimeter = a + b + c;
8877
+ const area = Math.abs((x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0)) / 2;
8878
+ if (perimeter === 0 || area === 0)
8879
+ return triangle;
8880
+ const inradius = (2 * area) / perimeter;
8881
+ const cx = (a * x0 + b * x1 + c * x2) / perimeter;
8882
+ const cy = (a * y0 + b * y1 + c * y2) / perimeter;
8883
+ const scale = 1 + distance / Math.max(inradius, 2 * distance);
8884
+ return triangle.map(([x, y]) => [cx + (x - cx) * scale, cy + (y - cy) * scale]);
8885
+ }
8886
+ function formatUnit(v) {
8887
+ return (Math.round(v * 100000) / 100000).toString();
8888
+ }
8653
8889
  function formatScale(v) {
8654
8890
  return (Math.round(v * 10000) / 10000).toString();
8655
8891
  }
@@ -8743,6 +8979,12 @@ class Feature {
8743
8979
  properties;
8744
8980
  patterns;
8745
8981
  geometry;
8982
+ /**
8983
+ * The boundary of a polygon for the fill-antialias outline, as open polylines — when it
8984
+ * differs from the polygon's rings, i.e. when the polygon was clipped to its tile: the
8985
+ * clipped edges along the tile border are not part of the outline.
8986
+ */
8987
+ outline;
8746
8988
  #bbox;
8747
8989
  constructor(opt) {
8748
8990
  this.type = opt.type;
@@ -8750,6 +8992,7 @@ class Feature {
8750
8992
  this.properties = opt.properties;
8751
8993
  this.patterns = opt.patterns;
8752
8994
  this.geometry = opt.geometry;
8995
+ this.outline = opt.outline;
8753
8996
  }
8754
8997
  getBbox() {
8755
8998
  if (this.#bbox)
@@ -9844,6 +10087,138 @@ function readUtf8(buf, pos, end) {
9844
10087
  return str;
9845
10088
  }
9846
10089
 
10090
+ /**
10091
+ * Clipping of vector tile geometry to the tile's own square.
10092
+ *
10093
+ * Vector tiles contain a buffer: geometry reaches a little beyond the tile, so a polygon on
10094
+ * a tile border exists in both tiles. MapLibre draws each tile clipped to its square (with the
10095
+ * stencil buffer), so nothing is drawn twice. Opaque fills hide the difference, translucent
10096
+ * ones do not: the overlap would be drawn with double opacity.
10097
+ */
10098
+ /** Whether any point lies outside of the square [min, max]². */
10099
+ function exceedsSquare(rings, min, max) {
10100
+ return rings.some((ring) => ring.some(({ x, y }) => x < min || x > max || y < min || y > max));
10101
+ }
10102
+ /**
10103
+ * Clips the rings of a polygon to the square [min, max]² (Sutherland–Hodgman). Rings that
10104
+ * vanish are dropped.
10105
+ */
10106
+ function clipPolygon(rings, min, max) {
10107
+ const result = [];
10108
+ for (const ring of rings) {
10109
+ let points = ring;
10110
+ points = clipAgainst(points, (p) => p.x >= min, (a, b) => lerpAtX(a, b, min));
10111
+ points = clipAgainst(points, (p) => p.x <= max, (a, b) => lerpAtX(a, b, max));
10112
+ points = clipAgainst(points, (p) => p.y >= min, (a, b) => lerpAtY(a, b, min));
10113
+ points = clipAgainst(points, (p) => p.y <= max, (a, b) => lerpAtY(a, b, max));
10114
+ if (points.length >= 3)
10115
+ result.push(points);
10116
+ }
10117
+ return result;
10118
+ }
10119
+ /**
10120
+ * The boundary of a polygon within the square [min, max]², as open polylines: the parts of
10121
+ * its rings inside the square, without the edges the clipping adds along the square's border.
10122
+ * (MapLibre's fill outline follows the original rings, cut off at the tile border.)
10123
+ */
10124
+ function clipPolygonOutline(rings, min, max) {
10125
+ const result = [];
10126
+ for (const ring of rings) {
10127
+ if (ring.length < 2)
10128
+ continue;
10129
+ const first = ring[0];
10130
+ const last = ring[ring.length - 1];
10131
+ const closed = first.x === last.x && first.y === last.y ? ring : [...ring, first];
10132
+ result.push(...clipLine(closed, min, max));
10133
+ }
10134
+ return result;
10135
+ }
10136
+ /** Clips a polyline to the square [min, max]², returning the parts inside of it. */
10137
+ function clipLine(line, min, max) {
10138
+ const parts = [];
10139
+ let current = [];
10140
+ for (let i = 0; i + 1 < line.length; i++) {
10141
+ const segment = clipSegment(line[i], line[i + 1], min, max);
10142
+ if (!segment) {
10143
+ if (current.length > 1)
10144
+ parts.push(current);
10145
+ current = [];
10146
+ continue;
10147
+ }
10148
+ const [a, b] = segment;
10149
+ const last = current[current.length - 1];
10150
+ if (last?.x !== a.x || last.y !== a.y) {
10151
+ if (current.length > 1)
10152
+ parts.push(current);
10153
+ current = [a];
10154
+ }
10155
+ current.push(b);
10156
+ // The segment left the square: the line continues elsewhere.
10157
+ if (b !== line[i + 1]) {
10158
+ parts.push(current);
10159
+ current = [];
10160
+ }
10161
+ }
10162
+ if (current.length > 1)
10163
+ parts.push(current);
10164
+ return parts;
10165
+ }
10166
+ /** Liang–Barsky: the part of the segment a→b inside the square, or undefined. */
10167
+ function clipSegment(a, b, min, max) {
10168
+ const dx = b.x - a.x;
10169
+ const dy = b.y - a.y;
10170
+ let t0 = 0;
10171
+ let t1 = 1;
10172
+ for (const [p, q] of [
10173
+ [-dx, a.x - min],
10174
+ [dx, max - a.x],
10175
+ [-dy, a.y - min],
10176
+ [dy, max - a.y],
10177
+ ]) {
10178
+ if (p === 0) {
10179
+ if (q < 0)
10180
+ return undefined;
10181
+ continue;
10182
+ }
10183
+ const t = q / p;
10184
+ if (p < 0) {
10185
+ if (t > t1)
10186
+ return undefined;
10187
+ if (t > t0)
10188
+ t0 = t;
10189
+ }
10190
+ else {
10191
+ if (t < t0)
10192
+ return undefined;
10193
+ if (t < t1)
10194
+ t1 = t;
10195
+ }
10196
+ }
10197
+ return [
10198
+ t0 === 0 ? a : { x: a.x + dx * t0, y: a.y + dy * t0 },
10199
+ t1 === 1 ? b : { x: a.x + dx * t1, y: a.y + dy * t1 },
10200
+ ];
10201
+ }
10202
+ function clipAgainst(points, inside, intersect) {
10203
+ const result = [];
10204
+ for (let i = 0; i < points.length; i++) {
10205
+ const current = points[i];
10206
+ const previous = points[(i + points.length - 1) % points.length];
10207
+ const currentInside = inside(current);
10208
+ if (currentInside !== inside(previous))
10209
+ result.push(intersect(previous, current));
10210
+ if (currentInside)
10211
+ result.push(current);
10212
+ }
10213
+ return result;
10214
+ }
10215
+ function lerpAtX(a, b, x) {
10216
+ return { x, y: a.y + ((b.y - a.y) * (x - a.x)) / (b.x - a.x) };
10217
+ }
10218
+ function lerpAtY(a, b, y) {
10219
+ return { x: a.x + ((b.x - a.x) * (y - a.y)) / (b.y - a.y), y };
10220
+ }
10221
+
9847
10222
  const TILE_EXTENT = 4096;
9848
10223
  const VTFeatureType = { Unknown: 0, Point: 1, LineString: 2, Polygon: 3 };
9849
10224
  async function loadVectorSource(source, job, layerFeatures) {
@@ -9851,11 +10226,8 @@ async function loadVectorSource(source, job, layerFeatures) {
9851
10226
  if (!tiles)
9852
10227
  return;
9853
10228
  const { width, height } = job.renderer;
9854
- const { zoom, center } = job.view;
9855
- const { zoomLevel, tileSize, tiles: tileCoordinates, } = calculateTileGrid(width, height, center, zoom, source.maxzoom);
9856
- await Promise.all(tileCoordinates.map(async ({ x, y, offsetX, offsetY }) => {
9857
- const offset = new Point2D(offsetX, offsetY);
9858
- const tile = await getTile(tiles[0], zoomLevel, x, y);
10229
+ await Promise.all(getTileProjections(source, job).map(async ({ x, y, z, project, clipToTile }) => {
10230
+ const tile = await getTile(tiles[0], z, x, y);
9859
10231
  if (!tile)
9860
10232
  return;
9861
10233
  const vectorTile = new VectorTile(new PbfReader(tile.buffer));
@@ -9867,9 +10239,6 @@ async function loadVectorSource(source, job, layerFeatures) {
9867
10239
  }
9868
10240
  for (let i = 0; i < layer.length; i++) {
9869
10241
  const featureSrc = layer.feature(i);
9870
- const geometry = featureSrc
9871
- .loadGeometry()
9872
- .map((ring) => ring.map((point) => new Point2D(point.x, point.y).scale(tileSize / TILE_EXTENT).translate(offset)));
9873
10242
  let type;
9874
10243
  let list;
9875
10244
  switch (featureSrc.type) {
@@ -9888,6 +10257,22 @@ async function loadVectorSource(source, job, layerFeatures) {
9888
10257
  list = features.polygons;
9889
10258
  break;
9890
10259
  }
10260
+ let rings = featureSrc.loadGeometry();
10261
+ let outline;
10262
+ // Clip polygons and lines to the tile, like MapLibre's stencil clipping: otherwise
10263
+ // the parts in the tile buffer are drawn twice, which shows when translucent.
10264
+ if (clipToTile && type !== 'Point' && exceedsSquare(rings, 0, TILE_EXTENT)) {
10265
+ if (type === 'Polygon') {
10266
+ outline = project('LineString', clipPolygonOutline(rings, 0, TILE_EXTENT));
10267
+ rings = clipPolygon(rings, 0, TILE_EXTENT);
10268
+ }
10269
+ else {
10270
+ rings = rings.flatMap((line) => clipLine(line, 0, TILE_EXTENT));
10271
+ }
10272
+ }
10273
+ const geometry = project(type, rings);
10274
+ if (geometry.length === 0)
10275
+ continue;
9891
10276
  // Split MultiPoint into individual Point features
9892
10277
  if (type === 'Point' && geometry.length > 1) {
9893
10278
  for (const ring of geometry) {
@@ -9905,6 +10290,7 @@ async function loadVectorSource(source, job, layerFeatures) {
9905
10290
  const feature = new Feature({
9906
10291
  type,
9907
10292
  geometry,
10293
+ outline,
9908
10294
  id: featureSrc.id,
9909
10295
  properties: featureSrc.properties,
9910
10296
  });
@@ -9915,9 +10301,41 @@ async function loadVectorSource(source, job, layerFeatures) {
9915
10301
  }
9916
10302
  }));
9917
10303
  }
10304
+ function getTileProjections(source, job) {
10305
+ const { width, height } = job.renderer;
10306
+ const { zoom, center } = job.view;
10307
+ const projection = job.projection;
10308
+ if (projection?.isGlobe) {
10309
+ // Never zoom level 0 on the globe: the left and right edge of its single tile coincide
10310
+ // (the antimeridian), which the horizon clipping cannot tell apart.
10311
+ const z = Math.max(1, Math.min(Math.floor(zoom), source.maxzoom ?? Infinity));
10312
+ const scale = 1 / (TILE_EXTENT * 2 ** z);
10313
+ return projection.coveringTiles(z).map(({ x, y }) => ({
10314
+ x,
10315
+ y,
10316
+ z,
10317
+ clipToTile: projection.isTileFullyVisible({ x, y, z }),
10318
+ project: (type, rings) => projection.projectGeometry(type, rings.map((ring) => ring.map((p) => [
10319
+ (x * TILE_EXTENT + p.x) * scale,
10320
+ (y * TILE_EXTENT + p.y) * scale,
10321
+ ]))),
10322
+ }));
10323
+ }
10324
+ const { zoomLevel, tileSize, tiles } = calculateTileGrid(width, height, center, zoom, source.maxzoom);
10325
+ return tiles.map(({ x, y, offsetX, offsetY }) => {
10326
+ const offset = new Point2D(offsetX, offsetY);
10327
+ return {
10328
+ x,
10329
+ y,
10330
+ z: zoomLevel,
10331
+ clipToTile: true,
10332
+ project: (_type, rings) => rings.map((ring) => ring.map((point) => new Point2D(point.x, point.y).scale(tileSize / TILE_EXTENT).translate(offset))),
10333
+ };
10334
+ });
10335
+ }
9918
10336
 
9919
10337
  function loadGeoJSONSource(options) {
9920
- const { sourceName, data, width, height, zoom, center, layerFeatures } = options;
10338
+ const { sourceName, data, width, height, zoom, center, layerFeatures, projection } = options;
9921
10339
  const existing = layerFeatures.get(sourceName);
9922
10340
  const features = existing ?? {
9923
10341
  points: [],
@@ -9930,9 +10348,17 @@ function loadGeoJSONSource(options) {
9930
10348
  layerFeatures.set(sourceName, features);
9931
10349
  const worldSize = 512 * 2 ** zoom;
9932
10350
  const centerMercator = new Point2D(center[0], center[1]).getProject2Pixel();
9933
- function projectCoord(coord) {
10351
+ function toMercator(coord) {
9934
10352
  const mercator = new Point2D(coord[0], coord[1]).getProject2Pixel();
9935
- return new Point2D((mercator.x - centerMercator.x) * worldSize + width / 2, (mercator.y - centerMercator.y) * worldSize + height / 2);
10353
+ return [mercator.x, mercator.y];
10354
+ }
10355
+ function project(type, rings) {
10356
+ if (projection?.isGlobe) {
10357
+ if (type === 'Polygon')
10358
+ rings = orientRings(rings);
10359
+ return projection.projectGeometry(type, rings);
10360
+ }
10361
+ return rings.map((ring) => ring.map(([x, y]) => new Point2D((x - centerMercator.x) * worldSize + width / 2, (y - centerMercator.y) * worldSize + height / 2)));
9936
10362
  }
9937
10363
  function makeFeature(type, geometry, id, properties) {
9938
10364
  const feature = new Feature({ type, geometry, id, properties });
@@ -9943,7 +10369,10 @@ function loadGeoJSONSource(options) {
9943
10369
  function extractPoints(geometry) {
9944
10370
  return geometry.flatMap((ring) => ring.map((p) => [p]));
9945
10371
  }
9946
- function addFeature(type, geometry, id, properties) {
10372
+ function addFeature(type, rings, id, properties) {
10373
+ const geometry = project(type, rings);
10374
+ if (geometry.length === 0)
10375
+ return;
9947
10376
  switch (type) {
9948
10377
  case 'Point': {
9949
10378
  const f = makeFeature('Point', geometry, id, properties);
@@ -9986,23 +10415,23 @@ function loadGeoJSONSource(options) {
9986
10415
  function processGeometry(geom, id, properties) {
9987
10416
  switch (geom.type) {
9988
10417
  case 'Point':
9989
- addFeature('Point', [[projectCoord(geom.coordinates)]], id, properties);
10418
+ addFeature('Point', [[toMercator(geom.coordinates)]], id, properties);
9990
10419
  break;
9991
10420
  case 'MultiPoint':
9992
- addFeature('Point', geom.coordinates.map((c) => [projectCoord(c)]), id, properties);
10421
+ addFeature('Point', geom.coordinates.map((c) => [toMercator(c)]), id, properties);
9993
10422
  break;
9994
10423
  case 'LineString':
9995
- addFeature('LineString', [geom.coordinates.map((c) => projectCoord(c))], id, properties);
10424
+ addFeature('LineString', [geom.coordinates.map((c) => toMercator(c))], id, properties);
9996
10425
  break;
9997
10426
  case 'MultiLineString':
9998
- addFeature('LineString', geom.coordinates.map((line) => line.map((c) => projectCoord(c))), id, properties);
10427
+ addFeature('LineString', geom.coordinates.map((line) => line.map((c) => toMercator(c))), id, properties);
9999
10428
  break;
10000
10429
  case 'Polygon':
10001
- addFeature('Polygon', geom.coordinates.map((ring) => ring.map((c) => projectCoord(c))), id, properties);
10430
+ addFeature('Polygon', geom.coordinates.map((ring) => ring.map((c) => toMercator(c))), id, properties);
10002
10431
  break;
10003
10432
  case 'MultiPolygon':
10004
10433
  for (const polygon of geom.coordinates) {
10005
- addFeature('Polygon', polygon.map((ring) => ring.map((c) => projectCoord(c))), id, properties);
10434
+ addFeature('Polygon', polygon.map((ring) => ring.map((c) => toMercator(c))), id, properties);
10006
10435
  }
10007
10436
  break;
10008
10437
  case 'GeometryCollection':
@@ -10026,6 +10455,22 @@ function loadGeoJSONSource(options) {
10026
10455
  break;
10027
10456
  }
10028
10457
  }
10458
+ /**
10459
+ * Orients polygon rings like vector tiles do (in mercator space, y pointing south: the
10460
+ * exterior ring clockwise, holes counter-clockwise), as the globe clipping relies on it.
10461
+ */
10462
+ function orientRings(rings) {
10463
+ return rings.map((ring, index) => {
10464
+ let area = 0;
10465
+ for (let i = 0; i < ring.length; i++) {
10466
+ const [x0, y0] = ring[i];
10467
+ const [x1, y1] = ring[(i + 1) % ring.length];
10468
+ area += x0 * y1 - x1 * y0;
10469
+ }
10470
+ const isExterior = index === 0;
10471
+ return area > 0 === isExterior ? ring : [...ring].reverse();
10472
+ });
10473
+ }
10029
10474
 
10030
10475
  /**
10031
10476
  * Base64-encode an ArrayBuffer (standard alphabet, with padding).
@@ -10063,6 +10508,23 @@ async function getRasterTiles(job, sourceName) {
10063
10508
  throw Error(`Invalid raster source "${sourceName}": expected type "raster" with a "tiles" array`);
10064
10509
  }
10065
10510
  const sourceUrl = source.tiles[0];
10511
+ const projection = job.projection;
10512
+ if (projection?.isGlobe) {
10513
+ // Never zoom level 0 on the globe: the left and right edge of its single tile coincide
10514
+ // (the antimeridian), which the horizon clipping cannot tell apart.
10515
+ const z = Math.max(1, Math.min(Math.floor(zoom), source.maxzoom ?? Infinity));
10516
+ const globeTiles = await Promise.all(projection.coveringTiles(z).map(async (id) => {
10517
+ const tile = await getTile(sourceUrl, id.z, id.x, id.y);
10518
+ if (!tile)
10519
+ return null;
10520
+ const triangles = projection.rasterTriangles(id);
10521
+ if (triangles.length === 0)
10522
+ return null;
10523
+ const dataUri = `data:${tile.contentType};base64,${arrayBufferToBase64(tile.buffer)}`;
10524
+ return { x: 0, y: 0, width: 1, height: 1, dataUri, triangles };
10525
+ }));
10526
+ return globeTiles.filter((tile) => tile !== null);
10527
+ }
10066
10528
  const { zoomLevel, tileSize, tiles } = calculateTileGrid(width, height, center, zoom, source.maxzoom);
10067
10529
  const rasterTiles = await Promise.all(tiles.map(async ({ x, y, offsetX, offsetY }) => {
10068
10530
  const tile = await getTile(sourceUrl, zoomLevel, x, y);
@@ -10166,6 +10628,7 @@ async function getLayerFeatures(job) {
10166
10628
  zoom,
10167
10629
  center,
10168
10630
  layerFeatures,
10631
+ projection: job.projection,
10169
10632
  });
10170
10633
  }
10171
10634
  break;
@@ -10297,6 +10760,594 @@ function getLayerStyles(layers) {
10297
10760
  return layers.map(createStyleLayer);
10298
10761
  }
10299
10762
 
10763
+ /**
10764
+ * Map projection: web mercator, MapLibre's globe ("vertical-perspective") or a blend of both.
10765
+ *
10766
+ * The math follows MapLibre GL JS (`vertical_perspective_transform.ts` and the
10767
+ * `_projection_globe.vertex.glsl` shader) for a camera without pitch, bearing or padding:
10768
+ * the globe is a sphere of radius `worldSize / (2π·cos(centerLat))` (so the map center has
10769
+ * the same scale as in mercator), viewed by a perspective camera `1.5 · height` pixels above
10770
+ * the map center. The globe → mercator transition blends the homogeneous clip-space positions
10771
+ * of both projections (x, y and w) *before* the perspective divide, exactly like the shader.
10772
+ *
10773
+ * All inputs are mercator world coordinates in the range 0..1 (`mx` east, `my` south).
10774
+ */
10775
+ /** MapLibre's default vertical field of view (`TransformHelper._fovInRadians`). */
10776
+ const FOV = 0.6435011087932844;
10777
+ /** What MapLibre expands `projection: { type: 'globe' }` into (`projection_factory.ts`). */
10778
+ const GLOBE_PRESET = [
10779
+ 'interpolate',
10780
+ ['linear'],
10781
+ ['zoom'],
10782
+ 11,
10783
+ 'vertical-perspective',
10784
+ 12,
10785
+ 'mercator',
10786
+ ];
10787
+ /** Below this globeness the far side of the globe is not clipped (MapLibre's `z_globeness_threshold`). */
10788
+ const CLIP_THRESHOLD = 0.2;
10789
+ /** Maximum distance (pixels) a straight segment may deviate from the curved globe surface. */
10790
+ const MAX_CURVE_ERROR_PX = 0.25;
10791
+ /** The same for the affine-mapped triangles of raster tiles (a coarser mesh keeps the SVG small). */
10792
+ const MAX_RASTER_ERROR_PX = 1;
10793
+ /** Upper bound for the number of cells per raster tile side. */
10794
+ const MAX_RASTER_CELLS = 16;
10795
+ /** Angle to go clockwise on screen (decreasing angle in the y-up view frame) from a0 to a1, in [0, 2π). */
10796
+ function clockwiseDelta(a0, a1) {
10797
+ const delta = (a0 - a1) % (2 * Math.PI);
10798
+ return delta < 0 ? delta + 2 * Math.PI : delta;
10799
+ }
10800
+ /**
10801
+ * Returns how "globe-like" the projection is at the given zoom, matching MapLibre's
10802
+ * `GlobeProjection.transitionState`: 0 = mercator, 1 = globe, in between = transition.
10803
+ */
10804
+ function getGlobeness(projection, zoom) {
10805
+ return evaluateGlobeness(projection?.type ?? 'mercator', zoom);
10806
+ }
10807
+ function evaluateGlobeness(value, zoom) {
10808
+ if (typeof value === 'string') {
10809
+ switch (value) {
10810
+ case 'globe':
10811
+ return evaluateGlobeness(GLOBE_PRESET, zoom);
10812
+ case 'vertical-perspective':
10813
+ return 1;
10814
+ default:
10815
+ return 0;
10816
+ }
10817
+ }
10818
+ if (!Array.isArray(value))
10819
+ return 0;
10820
+ const [op, ...args] = value;
10821
+ if (op === 'literal')
10822
+ return evaluateGlobeness(args[0], zoom);
10823
+ if (op === 'step') {
10824
+ // ["step", ["zoom"], output0, stop1, output1, ...]
10825
+ let result = args[1];
10826
+ for (let i = 2; i + 1 < args.length; i += 2) {
10827
+ if (zoom >= args[i])
10828
+ result = args[i + 1];
10829
+ }
10830
+ return evaluateGlobeness(result, zoom);
10831
+ }
10832
+ if (op === 'interpolate') {
10833
+ // ["interpolate", ["linear"] | ["exponential", base], ["zoom"], stop0, output0, ...]
10834
+ const interpolation = args[0];
10835
+ const base = interpolation[0] === 'exponential' ? interpolation[1] : 1;
10836
+ const stops = [];
10837
+ for (let i = 2; i + 1 < args.length; i += 2)
10838
+ stops.push([args[i], args[i + 1]]);
10839
+ if (stops.length === 0)
10840
+ return 0;
10841
+ if (zoom <= stops[0][0])
10842
+ return evaluateGlobeness(stops[0][1], zoom);
10843
+ for (let i = 1; i < stops.length; i++) {
10844
+ const [z1, out1] = stops[i];
10845
+ if (zoom > z1)
10846
+ continue;
10847
+ const [z0, out0] = stops[i - 1];
10848
+ const t = interpolationFactor(zoom, z0, z1, base);
10849
+ const g0 = evaluateGlobeness(out0, zoom);
10850
+ const g1 = evaluateGlobeness(out1, zoom);
10851
+ return g0 + (g1 - g0) * t;
10852
+ }
10853
+ return evaluateGlobeness(stops[stops.length - 1][1], zoom);
10854
+ }
10855
+ // A projection transition literal: [from, to, transition]
10856
+ if (value.length === 3 && typeof value[2] === 'number') {
10857
+ const g0 = evaluateGlobeness(value[0], zoom);
10858
+ const g1 = evaluateGlobeness(value[1], zoom);
10859
+ return g0 + (g1 - g0) * value[2];
10860
+ }
10861
+ return 0;
10862
+ }
10863
+ function interpolationFactor(input, lower, upper, base) {
10864
+ const difference = upper - lower;
10865
+ if (difference === 0)
10866
+ return 0;
10867
+ const progress = input - lower;
10868
+ if (base === 1)
10869
+ return progress / difference;
10870
+ return (Math.pow(base, progress) - 1) / (Math.pow(base, difference) - 1);
10871
+ }
10872
+ class Projection {
10873
+ width;
10874
+ height;
10875
+ zoom;
10876
+ /** 0 = mercator, 1 = globe, in between = transition (see {@link getGlobeness}). */
10877
+ globeness;
10878
+ /** Size of the whole world in pixels at the current zoom (mercator). */
10879
+ worldSize;
10880
+ /**
10881
+ * Maximum length (in mercator units) of a straight segment before it has to be
10882
+ * subdivided to follow the curvature of the globe.
10883
+ */
10884
+ maxSegmentLength;
10885
+ /** Whether geometry on the far side of the globe has to be removed. */
10886
+ clipsHorizon;
10887
+ #centerX;
10888
+ #centerY;
10889
+ #centerLng;
10890
+ #sinLat;
10891
+ #cosLat;
10892
+ /** Globe radius in pixels. */
10893
+ #radius;
10894
+ /** Distance of the camera to the map center in pixels. */
10895
+ #cameraDistance;
10896
+ /** Points of the unit sphere with a view-space z below this are behind the horizon. */
10897
+ #horizonZ;
10898
+ constructor(opt) {
10899
+ this.width = opt.width;
10900
+ this.height = opt.height;
10901
+ this.zoom = opt.zoom;
10902
+ this.globeness = Math.min(1, Math.max(0, opt.globeness ?? 0));
10903
+ this.worldSize = 512 * 2 ** opt.zoom;
10904
+ const center = new Point2D(opt.center[0], opt.center[1]).getProject2Pixel();
10905
+ this.#centerX = center.x;
10906
+ this.#centerY = center.y;
10907
+ const lat = (opt.center[1] * Math.PI) / 180;
10908
+ this.#centerLng = (opt.center[0] * Math.PI) / 180;
10909
+ this.#sinLat = Math.sin(lat);
10910
+ this.#cosLat = Math.cos(lat);
10911
+ this.#radius = this.worldSize / (2 * Math.PI * this.#cosLat);
10912
+ this.#cameraDistance = this.height / 2 / Math.tan(FOV / 2);
10913
+ this.#horizonZ = this.#radius / (this.#radius + this.#cameraDistance);
10914
+ this.clipsHorizon = this.globeness > CLIP_THRESHOLD;
10915
+ // A chord spanning the angle θ deviates R·θ²/8 from the sphere.
10916
+ const maxAngle = Math.sqrt((8 * MAX_CURVE_ERROR_PX) / this.#radius);
10917
+ this.maxSegmentLength = maxAngle / (2 * Math.PI);
10918
+ }
10919
+ static fromStyle(opt) {
10920
+ return new Projection({ ...opt, globeness: getGlobeness(opt.projection, opt.zoom) });
10921
+ }
10922
+ /** True when the globe projection is (at least partially) in effect. */
10923
+ get isGlobe() {
10924
+ return this.globeness > 0;
10925
+ }
10926
+ /**
10927
+ * The silhouette of the globe on screen, if the map has to be clipped to it.
10928
+ * (During the globe → mercator transition the silhouette is far outside the screen.)
10929
+ */
10930
+ get clipCircle() {
10931
+ if (!this.clipsHorizon)
10932
+ return undefined;
10933
+ const r = this.#radius;
10934
+ const d = this.#cameraDistance;
10935
+ const radius = (d * r) / Math.sqrt((d + r) * (d + r) - r * r);
10936
+ return { x: this.width / 2, y: this.height / 2, radius };
10937
+ }
10938
+ /** Mercator world coordinates → unit sphere in the view frame (x right, y up, z towards the camera). */
10939
+ toSphere(mx, my) {
10940
+ const dLng = mx * 2 * Math.PI - Math.PI - this.#centerLng;
10941
+ const lat = 2 * Math.atan(Math.exp(Math.PI - my * 2 * Math.PI)) - Math.PI / 2;
10942
+ const cosLat = Math.cos(lat);
10943
+ const x = Math.sin(dLng) * cosLat;
10944
+ const y = Math.sin(lat);
10945
+ const z = Math.cos(dLng) * cosLat;
10946
+ return [x, y * this.#cosLat - z * this.#sinLat, y * this.#sinLat + z * this.#cosLat];
10947
+ }
10948
+ /** Inverse of {@link toSphere}. */
10949
+ fromSphere(v) {
10950
+ const y = v[1] * this.#cosLat + v[2] * this.#sinLat;
10951
+ const z = -v[1] * this.#sinLat + v[2] * this.#cosLat;
10952
+ const lat = Math.asin(Math.max(-1, Math.min(1, y)));
10953
+ const lng = Math.atan2(v[0], z) + this.#centerLng;
10954
+ const mx = (lng + Math.PI) / (2 * Math.PI);
10955
+ const my = (Math.PI - Math.log(Math.tan(Math.PI / 4 + lat / 2))) / (2 * Math.PI);
10956
+ return [mx - Math.floor(mx), my];
10957
+ }
10958
+ isVisible(v) {
10959
+ return !this.clipsHorizon || v[2] >= this.#horizonZ;
10960
+ }
10961
+ /** Screen position of a mercator point. */
10962
+ project(mx, my, v) {
10963
+ // Flat (mercator) position, relative to the screen center. Use the world copy
10964
+ // nearest to the center.
10965
+ let dx = mx - this.#centerX;
10966
+ dx -= Math.round(dx);
10967
+ const flatX = dx * this.worldSize;
10968
+ const flatY = (my - this.#centerY) * this.worldSize;
10969
+ const t = this.globeness;
10970
+ if (t === 0)
10971
+ return new Point2D(flatX + this.width / 2, flatY + this.height / 2);
10972
+ v ??= this.toSphere(mx, my);
10973
+ const d = this.#cameraDistance;
10974
+ const r = this.#radius;
10975
+ // Homogeneous coordinates (x, y, w) of the globe position; the flat one is (d·x, d·y, d).
10976
+ const globeX = d * r * v[0];
10977
+ const globeY = -d * r * v[1];
10978
+ const globeW = d + r * (1 - v[2]);
10979
+ const w = d + (globeW - d) * t;
10980
+ return new Point2D((d * flatX + (globeX - d * flatX) * t) / w + this.width / 2, (d * flatY + (globeY - d * flatY) * t) / w + this.height / 2);
10981
+ }
10982
+ /**
10983
+ * Projects feature geometry given in mercator world coordinates to screen pixels.
10984
+ * On the globe, segments are subdivided to follow the curvature, lines are cut at
10985
+ * the horizon and polygons are clipped to the visible hemisphere (the clipped part of
10986
+ * the boundary follows the globe's silhouette).
10987
+ */
10988
+ projectGeometry(type, rings) {
10989
+ if (!this.isGlobe)
10990
+ return rings.map((ring) => ring.map(([mx, my]) => this.project(mx, my)));
10991
+ switch (type) {
10992
+ case 'Point':
10993
+ return rings
10994
+ .map((ring) => ring.flatMap(([mx, my]) => {
10995
+ const v = this.toSphere(mx, my);
10996
+ return this.isVisible(v) ? [this.project(mx, my, v)] : [];
10997
+ }))
10998
+ .filter((ring) => ring.length > 0);
10999
+ case 'LineString':
11000
+ return rings.flatMap((ring) => this.#clipLine(this.#densify(ring, false)));
11001
+ case 'Polygon':
11002
+ return this.#clipPolygon(rings.map((ring) => this.#densify(ring, true)));
11003
+ }
11004
+ }
11005
+ /** Subdivides long segments (in mercator space) and lifts the points onto the sphere. */
11006
+ #densify(ring, closed) {
11007
+ const maxLength = this.maxSegmentLength;
11008
+ const result = [];
11009
+ const count = ring.length;
11010
+ const segments = closed ? count : count - 1;
11011
+ for (let i = 0; i < count; i++) {
11012
+ const [x0, y0] = ring[i];
11013
+ result.push({ mx: x0, my: y0, v: this.toSphere(x0, y0) });
11014
+ if (i >= segments)
11015
+ continue;
11016
+ const [x1, y1] = ring[(i + 1) % count];
11017
+ const steps = Math.ceil(Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0)) / maxLength);
11018
+ for (let s = 1; s < steps; s++) {
11019
+ const mx = x0 + ((x1 - x0) * s) / steps;
11020
+ const my = y0 + ((y1 - y0) * s) / steps;
11021
+ result.push({ mx, my, v: this.toSphere(mx, my) });
11022
+ }
11023
+ }
11024
+ return result;
11025
+ }
11026
+ /** Point on the segment a→b where it crosses the horizon plane. */
11027
+ #horizonCrossing(a, b) {
11028
+ const s = (a.v[2] - this.#horizonZ) / (a.v[2] - b.v[2]);
11029
+ const mx = a.mx + (b.mx - a.mx) * s;
11030
+ const my = a.my + (b.my - a.my) * s;
11031
+ return { mx, my, v: this.toSphere(mx, my) };
11032
+ }
11033
+ /** Splits a line into its visible parts. */
11034
+ #clipLine(points) {
11035
+ if (!this.clipsHorizon)
11036
+ return [points.map((p) => this.project(p.mx, p.my, p.v))];
11037
+ const parts = [];
11038
+ let current = [];
11039
+ for (let i = 0; i < points.length; i++) {
11040
+ const p = points[i];
11041
+ const visible = this.isVisible(p.v);
11042
+ const prev = i > 0 ? points[i - 1] : undefined;
11043
+ if (prev && this.isVisible(prev.v) !== visible) {
11044
+ const c = this.#horizonCrossing(prev, p);
11045
+ current.push(this.project(c.mx, c.my, c.v));
11046
+ if (!visible) {
11047
+ parts.push(current);
11048
+ current = [];
11049
+ }
11050
+ }
11051
+ if (visible)
11052
+ current.push(this.project(p.mx, p.my, p.v));
11053
+ }
11054
+ if (current.length > 0)
11055
+ parts.push(current);
11056
+ return parts.filter((part) => part.length > 1);
11057
+ }
11058
+ /**
11059
+ * Clips the rings of a polygon to the visible hemisphere.
11060
+ *
11061
+ * Every ring crossing the horizon is cut into its visible chains, each running from an
11062
+ * entry point to an exit point on the horizon. The chains are then reconnected along the
11063
+ * silhouette: from an exit point, the silhouette is followed to the nearest entry point
11064
+ * (of any chain of any ring). Rings follow the vector tile winding (exterior rings
11065
+ * clockwise on screen, holes counter-clockwise), which keeps the filled area on the right
11066
+ * of every ring, so the silhouette is always walked clockwise.
11067
+ */
11068
+ #clipPolygon(rings) {
11069
+ const project = (p) => this.project(p.mx, p.my, p.v);
11070
+ if (!this.clipsHorizon)
11071
+ return rings.map((ring) => ring.map(project));
11072
+ const result = [];
11073
+ const chains = [];
11074
+ const angleOf = (p) => Math.atan2(p.v[1], p.v[0]);
11075
+ for (const ring of rings) {
11076
+ const count = ring.length;
11077
+ const visible = ring.map((p) => this.isVisible(p.v));
11078
+ // Start walking at an entry point, so every chain is complete.
11079
+ let start = -1;
11080
+ for (let i = 0; i < count; i++) {
11081
+ if (visible[i] && !visible[(i + count - 1) % count]) {
11082
+ start = i;
11083
+ break;
11084
+ }
11085
+ }
11086
+ if (start < 0) {
11087
+ // No crossing: the ring is either completely visible or completely hidden.
11088
+ if (visible[0])
11089
+ result.push(ring.map(project));
11090
+ continue;
11091
+ }
11092
+ let chain;
11093
+ for (let k = 0; k < count; k++) {
11094
+ const i = (start + k) % count;
11095
+ const p = ring[i];
11096
+ const prev = ring[(i + count - 1) % count];
11097
+ if (visible[i]) {
11098
+ if (!chain) {
11099
+ const c = this.#horizonCrossing(prev, p);
11100
+ chain = { entry: angleOf(c), exit: 0, points: [project(c)] };
11101
+ }
11102
+ chain.points.push(project(p));
11103
+ }
11104
+ else if (chain) {
11105
+ const c = this.#horizonCrossing(prev, p);
11106
+ chain.points.push(project(c));
11107
+ chain.exit = angleOf(c);
11108
+ chains.push(chain);
11109
+ chain = undefined;
11110
+ }
11111
+ }
11112
+ }
11113
+ // Reconnect the chains along the silhouette.
11114
+ const used = new Set();
11115
+ for (const first of chains) {
11116
+ if (used.has(first))
11117
+ continue;
11118
+ const ring = [];
11119
+ let chain = first;
11120
+ for (;;) {
11121
+ used.add(chain);
11122
+ ring.push(...chain.points);
11123
+ let next = first;
11124
+ let bestDelta = clockwiseDelta(chain.exit, first.entry);
11125
+ for (const candidate of chains) {
11126
+ if (used.has(candidate))
11127
+ continue;
11128
+ const delta = clockwiseDelta(chain.exit, candidate.entry);
11129
+ if (delta < bestDelta) {
11130
+ bestDelta = delta;
11131
+ next = candidate;
11132
+ }
11133
+ }
11134
+ this.#pushArc(ring, chain.exit, next.entry);
11135
+ if (next === first)
11136
+ break;
11137
+ chain = next;
11138
+ }
11139
+ result.push(ring);
11140
+ }
11141
+ return result;
11142
+ }
11143
+ /**
11144
+ * Appends points along the horizon circle, clockwise on screen (decreasing angle in the
11145
+ * y-up view frame) from angle a0 to a1.
11146
+ */
11147
+ #pushArc(result, a0, a1) {
11148
+ const delta = clockwiseDelta(a0, a1);
11149
+ const steps = Math.ceil(delta / (Math.PI / 90));
11150
+ const h = this.#horizonZ;
11151
+ const rho = Math.sqrt(1 - h * h);
11152
+ for (let s = 1; s < steps; s++) {
11153
+ const a = a0 - (delta * s) / steps;
11154
+ const v = [rho * Math.cos(a), rho * Math.sin(a), h];
11155
+ const [mx, my] = this.fromSphere(v);
11156
+ result.push(this.project(mx, my, v));
11157
+ }
11158
+ }
11159
+ /**
11160
+ * Splits a raster tile into a mesh of triangles, each small enough to be drawn with an
11161
+ * affine transform (see {@link RasterTriangle}). An affine transform is fully determined
11162
+ * by three points, so every triangle meets its projected corners exactly and neighbouring
11163
+ * triangles share their edges. Cells on the far side of the globe are dropped.
11164
+ */
11165
+ rasterTriangles(tile) {
11166
+ const tileSize = 1 / 2 ** tile.z;
11167
+ const maxLength = this.maxSegmentLength * Math.sqrt(MAX_RASTER_ERROR_PX / MAX_CURVE_ERROR_PX);
11168
+ const n = Math.max(1, Math.min(MAX_RASTER_CELLS, Math.ceil(tileSize / maxLength)));
11169
+ // Project the (n+1)² grid corners once.
11170
+ const corners = [];
11171
+ for (let j = 0; j <= n; j++) {
11172
+ for (let i = 0; i <= n; i++) {
11173
+ const mx = (tile.x + i / n) * tileSize;
11174
+ const my = (tile.y + j / n) * tileSize;
11175
+ let v = this.toSphere(mx, my);
11176
+ if (this.isVisible(v)) {
11177
+ corners.push(this.project(mx, my, v));
11178
+ }
11179
+ else {
11180
+ // Pull the corner onto the horizon, so cells at the silhouette end there.
11181
+ if (this.#isBehind(v)) {
11182
+ corners.push(undefined);
11183
+ continue;
11184
+ }
11185
+ v = this.#clampToHorizon(v);
11186
+ const [cx, cy] = this.fromSphere(v);
11187
+ corners.push(this.project(cx, cy, v));
11188
+ }
11189
+ }
11190
+ }
11191
+ const triangles = [];
11192
+ for (let j = 0; j < n; j++) {
11193
+ for (let i = 0; i < n; i++) {
11194
+ const p00 = corners[j * (n + 1) + i];
11195
+ const p10 = corners[j * (n + 1) + i + 1];
11196
+ const p01 = corners[(j + 1) * (n + 1) + i];
11197
+ const p11 = corners[(j + 1) * (n + 1) + i + 1];
11198
+ if (!p00 || !p10 || !p01 || !p11)
11199
+ continue;
11200
+ const minX = Math.min(p00.x, p10.x, p01.x, p11.x);
11201
+ const maxX = Math.max(p00.x, p10.x, p01.x, p11.x);
11202
+ const minY = Math.min(p00.y, p10.y, p01.y, p11.y);
11203
+ const maxY = Math.max(p00.y, p10.y, p01.y, p11.y);
11204
+ if (maxX < 0 || minX > this.width || maxY < 0 || minY > this.height)
11205
+ continue;
11206
+ if (!this.#isCellVisible(tile, n, i, j))
11207
+ continue;
11208
+ const u0 = i / n;
11209
+ const v0 = j / n;
11210
+ const u1 = (i + 1) / n;
11211
+ const v1 = (j + 1) / n;
11212
+ triangles.push({
11213
+ source: [
11214
+ [u0, v0],
11215
+ [u1, v0],
11216
+ [u1, v1],
11217
+ ],
11218
+ target: [
11219
+ [p00.x, p00.y],
11220
+ [p10.x, p10.y],
11221
+ [p11.x, p11.y],
11222
+ ],
11223
+ }, {
11224
+ source: [
11225
+ [u0, v0],
11226
+ [u1, v1],
11227
+ [u0, v1],
11228
+ ],
11229
+ target: [
11230
+ [p00.x, p00.y],
11231
+ [p11.x, p11.y],
11232
+ [p01.x, p01.y],
11233
+ ],
11234
+ });
11235
+ }
11236
+ }
11237
+ return triangles;
11238
+ }
11239
+ /** Whether any part of a raster cell is on the visible side of the globe. */
11240
+ #isCellVisible(tile, n, i, j) {
11241
+ if (!this.clipsHorizon)
11242
+ return true;
11243
+ const tileSize = 1 / 2 ** tile.z;
11244
+ for (let dj = 0; dj <= 2; dj++) {
11245
+ for (let di = 0; di <= 2; di++) {
11246
+ const mx = (tile.x + (i + di / 2) / n) * tileSize;
11247
+ const my = (tile.y + (j + dj / 2) / n) * tileSize;
11248
+ if (this.isVisible(this.toSphere(mx, my)))
11249
+ return true;
11250
+ }
11251
+ }
11252
+ return false;
11253
+ }
11254
+ /** Moves a point on the far side of the globe onto the horizon (keeping its direction on screen). */
11255
+ #clampToHorizon(v) {
11256
+ const h = this.#horizonZ;
11257
+ const rho = Math.sqrt(1 - h * h);
11258
+ const len = Math.hypot(v[0], v[1]);
11259
+ if (len < 1e-12)
11260
+ return [rho, 0, h];
11261
+ return [(v[0] / len) * rho, (v[1] / len) * rho, h];
11262
+ }
11263
+ /** Points almost opposite the view direction have no meaningful place on the horizon. */
11264
+ #isBehind(v) {
11265
+ return Math.hypot(v[0], v[1]) < 1e-9;
11266
+ }
11267
+ /**
11268
+ * Tiles at zoom level `z` that are visible on screen, found by walking down the tile
11269
+ * pyramid and projecting a grid of sample points of each tile.
11270
+ */
11271
+ coveringTiles(z) {
11272
+ const result = [];
11273
+ const visit = (tz, tx, ty) => {
11274
+ if (tz >= 2 && !this.#isTileVisible(tz, tx, ty))
11275
+ return;
11276
+ if (tz === z) {
11277
+ result.push({ x: tx, y: ty, z: tz });
11278
+ return;
11279
+ }
11280
+ for (let dy = 0; dy < 2; dy++) {
11281
+ for (let dx = 0; dx < 2; dx++)
11282
+ visit(tz + 1, tx * 2 + dx, ty * 2 + dy);
11283
+ }
11284
+ };
11285
+ visit(0, 0, 0);
11286
+ return result;
11287
+ }
11288
+ /**
11289
+ * Whether a tile, including a margin around it (the tile buffer, as a fraction of the tile
11290
+ * size), lies completely on the visible side of the globe, so no horizon clipping happens.
11291
+ */
11292
+ isTileFullyVisible(tile, margin = 0.1) {
11293
+ if (!this.clipsHorizon)
11294
+ return true;
11295
+ const size = 1 / 2 ** tile.z;
11296
+ const samples = 8;
11297
+ for (let j = 0; j <= samples; j++) {
11298
+ for (let i = 0; i <= samples; i++) {
11299
+ if (i !== 0 && i !== samples && j !== 0 && j !== samples)
11300
+ continue; // the border only
11301
+ const mx = (tile.x - margin + ((1 + 2 * margin) * i) / samples) * size;
11302
+ const my = (tile.y - margin + ((1 + 2 * margin) * j) / samples) * size;
11303
+ if (!this.isVisible(this.toSphere(mx, my)))
11304
+ return false;
11305
+ }
11306
+ }
11307
+ return true;
11308
+ }
11309
+ #isTileVisible(z, x, y) {
11310
+ const samples = 6;
11311
+ const size = 1 / 2 ** z;
11312
+ let minX = Infinity;
11313
+ let minY = Infinity;
11314
+ let maxX = -Infinity;
11315
+ let maxY = -Infinity;
11316
+ const addSample = (mx, my) => {
11317
+ const v = this.toSphere(mx, my);
11318
+ if (!this.isVisible(v))
11319
+ return;
11320
+ const p = this.project(mx, my, v);
11321
+ if (p.x < minX)
11322
+ minX = p.x;
11323
+ if (p.y < minY)
11324
+ minY = p.y;
11325
+ if (p.x > maxX)
11326
+ maxX = p.x;
11327
+ if (p.y > maxY)
11328
+ maxY = p.y;
11329
+ };
11330
+ for (let j = 0; j <= samples; j++) {
11331
+ for (let i = 0; i <= samples; i++)
11332
+ addSample((x + i / samples) * size, (y + j / samples) * size);
11333
+ }
11334
+ // Zoomed in, the visible part of the globe can be much smaller than the sample spacing.
11335
+ // So also sample the point of the tile closest to the map center.
11336
+ const x0 = x * size;
11337
+ const centerX = this.#centerX - Math.round(this.#centerX - (x0 + size / 2));
11338
+ addSample(clamp(centerX, x0, x0 + size), clamp(this.#centerY, y * size, (y + 1) * size));
11339
+ // A generous margin: the samples may miss the part of the tile at the silhouette.
11340
+ const margin = Math.max(maxX - minX, maxY - minY) / samples + 1;
11341
+ return (minX - margin <= this.width &&
11342
+ maxX + margin >= 0 &&
11343
+ minY - margin <= this.height &&
11344
+ maxY + margin >= 0);
11345
+ }
11346
+ }
11347
+ function clamp(value, min, max) {
11348
+ return Math.min(max, Math.max(min, value));
11349
+ }
11350
+
10300
11351
  function resolveTokens(text, properties) {
10301
11352
  return text.replace(/\{([^}]+)\}/g, (_, key) => {
10302
11353
  const value = properties[key];
@@ -10310,6 +11361,16 @@ function resolveTokens(text, properties) {
10310
11361
  });
10311
11362
  }
10312
11363
  async function renderMap(job) {
11364
+ job.projection ??= Projection.fromStyle({
11365
+ width: job.renderer.width,
11366
+ height: job.renderer.height,
11367
+ center: job.view.center,
11368
+ zoom: job.view.zoom,
11369
+ projection: job.style.projection,
11370
+ });
11371
+ const clipCircle = job.projection.clipCircle;
11372
+ if (clipCircle)
11373
+ job.renderer.setClipCircle?.(clipCircle);
10313
11374
  await render(job);
10314
11375
  return job.renderer.getString();
10315
11376
  }