@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.
@@ -491,10 +491,7 @@ var v8_default = {
491
491
  "type": "enum",
492
492
  "values": { "image": {} }
493
493
  },
494
- "url": {
495
- "required": true,
496
- "type": "string"
497
- },
494
+ "url": { "type": "string" },
498
495
  "coordinates": {
499
496
  "required": true,
500
497
  "type": "array",
@@ -628,19 +625,28 @@ var v8_default = {
628
625
  },
629
626
  "property-type": "data-constant"
630
627
  } },
631
- "layout_fill-extrusion": { "visibility": {
632
- "type": "enum",
633
- "values": {
634
- "visible": {},
635
- "none": {}
636
- },
637
- "default": "visible",
638
- "expression": {
639
- "interpolated": false,
640
- "parameters": ["global-state"]
628
+ "layout_fill-extrusion": {
629
+ "visibility": {
630
+ "type": "enum",
631
+ "values": {
632
+ "visible": {},
633
+ "none": {}
634
+ },
635
+ "default": "visible",
636
+ "expression": {
637
+ "interpolated": false,
638
+ "parameters": ["global-state"]
639
+ },
640
+ "property-type": "data-constant"
641
641
  },
642
- "property-type": "data-constant"
643
- } },
642
+ "fill-extrusion-rounded-corner-distance": {
643
+ "type": "number",
644
+ "default": 0,
645
+ "minimum": 0,
646
+ "units": "meters",
647
+ "property-type": "constant"
648
+ }
649
+ },
644
650
  layout_line: {
645
651
  "line-cap": {
646
652
  "type": "enum",
@@ -825,9 +831,9 @@ var v8_default = {
825
831
  "requires": ["icon-image"],
826
832
  "expression": {
827
833
  "interpolated": false,
828
- "parameters": ["zoom"]
834
+ "parameters": ["zoom", "feature"]
829
835
  },
830
- "property-type": "data-constant"
836
+ "property-type": "data-driven"
831
837
  },
832
838
  "icon-size": {
833
839
  "type": "number",
@@ -1288,6 +1294,31 @@ var v8_default = {
1288
1294
  },
1289
1295
  "property-type": "data-constant"
1290
1296
  },
1297
+ "symbol-height-offset": {
1298
+ "type": "number",
1299
+ "default": 0,
1300
+ "units": "meters",
1301
+ "requires": [{ "symbol-placement": ["point"] }],
1302
+ "expression": {
1303
+ "interpolated": true,
1304
+ "parameters": ["zoom", "feature"]
1305
+ },
1306
+ "property-type": "data-driven"
1307
+ },
1308
+ "symbol-height-anchor": {
1309
+ "type": "enum",
1310
+ "values": {
1311
+ "ground": {},
1312
+ "absolute": {}
1313
+ },
1314
+ "default": "ground",
1315
+ "requires": ["symbol-height-offset"],
1316
+ "expression": {
1317
+ "interpolated": false,
1318
+ "parameters": ["zoom"]
1319
+ },
1320
+ "property-type": "data-constant"
1321
+ },
1291
1322
  "visibility": {
1292
1323
  "type": "enum",
1293
1324
  "values": {
@@ -1763,7 +1794,6 @@ var v8_default = {
1763
1794
  "fill-extrusion-height": {
1764
1795
  "type": "number",
1765
1796
  "default": 0,
1766
- "minimum": 0,
1767
1797
  "units": "meters",
1768
1798
  "transition": true,
1769
1799
  "expression": {
@@ -1779,7 +1809,6 @@ var v8_default = {
1779
1809
  "fill-extrusion-base": {
1780
1810
  "type": "number",
1781
1811
  "default": 0,
1782
- "minimum": 0,
1783
1812
  "units": "meters",
1784
1813
  "transition": true,
1785
1814
  "requires": ["fill-extrusion-height"],
@@ -2814,44 +2843,10 @@ var v8_default = {
2814
2843
  }
2815
2844
  }
2816
2845
  };
2817
- //#endregion
2846
+
2818
2847
  //#region src/reference/latest.ts
2819
2848
  const latest = v8_default;
2820
- //#endregion
2821
- //#region src/expression/parsing_error.ts
2822
- var ExpressionParsingError = class extends Error {
2823
- constructor(key, message) {
2824
- super(message);
2825
- this.message = message;
2826
- this.key = key;
2827
- }
2828
- };
2829
- //#endregion
2830
- //#region src/expression/scope.ts
2831
- /**
2832
- * Tracks `let` bindings during expression parsing.
2833
- * @private
2834
- */
2835
- var Scope = class Scope {
2836
- constructor(parent, bindings = []) {
2837
- this.parent = parent;
2838
- this.bindings = {};
2839
- for (const [name, expression] of bindings) this.bindings[name] = expression;
2840
- }
2841
- concat(bindings) {
2842
- return new Scope(this, bindings);
2843
- }
2844
- get(name) {
2845
- if (this.bindings[name]) return this.bindings[name];
2846
- if (this.parent) return this.parent.get(name);
2847
- throw new Error(`${name} not found in scope.`);
2848
- }
2849
- has(name) {
2850
- if (this.bindings[name]) return true;
2851
- return this.parent ? this.parent.has(name) : false;
2852
- }
2853
- };
2854
- //#endregion
2849
+
2855
2850
  //#region src/expression/types.ts
2856
2851
  const NullType = { kind: "null" };
2857
2852
  const NumberType = { kind: "number" };
@@ -2947,7 +2942,7 @@ function verifyType(provided, sample) {
2947
2942
  if (provided.kind === "array" && sample.kind === "array") return provided.itemType.kind === sample.itemType.kind && typeof provided.N === "number";
2948
2943
  return provided.kind === sample.kind;
2949
2944
  }
2950
- //#endregion
2945
+
2951
2946
  //#region src/expression/types/color_spaces.ts
2952
2947
  const Xn = .96422;
2953
2948
  const Yn = 1;
@@ -3042,7 +3037,7 @@ function hslToRgb([h, s, l, alpha]) {
3042
3037
  alpha
3043
3038
  ];
3044
3039
  }
3045
- //#endregion
3040
+
3046
3041
  //#region src/util/get_own.ts
3047
3042
  const hasOwnProperty = Object.hasOwn || function hasOwnProperty(object, key) {
3048
3043
  return Object.prototype.hasOwnProperty.call(object, key);
@@ -3050,7 +3045,7 @@ const hasOwnProperty = Object.hasOwn || function hasOwnProperty(object, key) {
3050
3045
  function getOwn(object, key) {
3051
3046
  return hasOwnProperty(object, key) ? object[key] : void 0;
3052
3047
  }
3053
- //#endregion
3048
+
3054
3049
  //#region src/expression/types/parse_css_color.ts
3055
3050
  /**
3056
3051
  * CSS color parser compliant with CSS Color 4 Specification.
@@ -3129,9 +3124,9 @@ function parseCssColor(input) {
3129
3124
  const maxValue = valFormat === "%%%" ? 100 : valFormat === "" ? 255 : 0;
3130
3125
  if (maxValue) {
3131
3126
  const rgba = [
3132
- clamp(+r / maxValue, 0, 1),
3133
- clamp(+g / maxValue, 0, 1),
3134
- clamp(+b / maxValue, 0, 1),
3127
+ clamp$1(+r / maxValue, 0, 1),
3128
+ clamp$1(+g / maxValue, 0, 1),
3129
+ clamp$1(+b / maxValue, 0, 1),
3135
3130
  a ? parseAlpha(+a, ap) : 1
3136
3131
  ];
3137
3132
  if (validateNumbers(rgba)) return rgba;
@@ -3151,8 +3146,8 @@ function parseCssColor(input) {
3151
3146
  if (argFormat === " " || argFormat === " /" || argFormat === ",," || argFormat === ",,,") {
3152
3147
  const hsla = [
3153
3148
  +h,
3154
- clamp(+s, 0, 100),
3155
- clamp(+l, 0, 100),
3149
+ clamp$1(+s, 0, 100),
3150
+ clamp$1(+l, 0, 100),
3156
3151
  a ? parseAlpha(+a, ap) : 1
3157
3152
  ];
3158
3153
  if (validateNumbers(hsla)) return hslToRgb(hsla);
@@ -3163,9 +3158,9 @@ function parseHex(hex) {
3163
3158
  return parseInt(hex.padEnd(2, hex), 16) / 255;
3164
3159
  }
3165
3160
  function parseAlpha(a, asPercentage) {
3166
- return clamp(asPercentage ? a / 100 : a, 0, 1);
3161
+ return clamp$1(asPercentage ? a / 100 : a, 0, 1);
3167
3162
  }
3168
- function clamp(n, min, max) {
3163
+ function clamp$1(n, min, max) {
3169
3164
  return Math.min(Math.max(min, n), max);
3170
3165
  }
3171
3166
  /**
@@ -3928,7 +3923,7 @@ const namedColors = {
3928
3923
  50
3929
3924
  ]
3930
3925
  };
3931
- //#endregion
3926
+
3932
3927
  //#region src/util/interpolate-primitives.ts
3933
3928
  function interpolateNumber(from, to, t) {
3934
3929
  return from + t * (to - from);
@@ -3938,7 +3933,7 @@ function interpolateArray(from, to, t) {
3938
3933
  return interpolateNumber(d, to[i], t);
3939
3934
  });
3940
3935
  }
3941
- //#endregion
3936
+
3942
3937
  //#region src/expression/types/color.ts
3943
3938
  /**
3944
3939
  * Checks whether the specified color space is one of the supported interpolation color spaces.
@@ -4122,26 +4117,7 @@ var Color$1 = class Color {
4122
4117
  }
4123
4118
  }
4124
4119
  };
4125
- //#endregion
4126
- //#region src/expression/types/collator.ts
4127
- var Collator = class {
4128
- constructor(caseSensitive, diacriticSensitive, locale) {
4129
- if (caseSensitive) this.sensitivity = diacriticSensitive ? "variant" : "case";
4130
- else this.sensitivity = diacriticSensitive ? "accent" : "base";
4131
- this.locale = locale;
4132
- this.collator = new Intl.Collator(this.locale ? this.locale : [], {
4133
- sensitivity: this.sensitivity,
4134
- usage: "search"
4135
- });
4136
- }
4137
- compare(lhs, rhs) {
4138
- return this.collator.compare(lhs, rhs);
4139
- }
4140
- resolvedLocale() {
4141
- return new Intl.Collator(this.locale ? this.locale : []).resolvedOptions().locale;
4142
- }
4143
- };
4144
- //#endregion
4120
+
4145
4121
  //#region src/expression/types/formatted.ts
4146
4122
  const VERTICAL_ALIGN_OPTIONS = [
4147
4123
  "bottom",
@@ -4178,7 +4154,7 @@ var Formatted = class Formatted {
4178
4154
  return this.sections.map((section) => section.text).join("");
4179
4155
  }
4180
4156
  };
4181
- //#endregion
4157
+
4182
4158
  //#region src/expression/types/padding.ts
4183
4159
  /**
4184
4160
  * A set of four numbers representing padding around a box. Create instances from
@@ -4222,14 +4198,12 @@ var Padding = class Padding {
4222
4198
  input[1]
4223
4199
  ];
4224
4200
  break;
4225
- case 3:
4226
- input = [
4227
- input[0],
4228
- input[1],
4229
- input[2],
4230
- input[1]
4231
- ];
4232
- break;
4201
+ case 3: input = [
4202
+ input[0],
4203
+ input[1],
4204
+ input[2],
4205
+ input[1]
4206
+ ];
4233
4207
  }
4234
4208
  return new Padding(input);
4235
4209
  }
@@ -4240,7 +4214,7 @@ var Padding = class Padding {
4240
4214
  return new Padding(interpolateArray(from.values, to.values, t));
4241
4215
  }
4242
4216
  };
4243
- //#endregion
4217
+
4244
4218
  //#region src/expression/types/number_array.ts
4245
4219
  /**
4246
4220
  * An array of numbers. Create instances from
@@ -4270,7 +4244,7 @@ var NumberArray = class NumberArray {
4270
4244
  return new NumberArray(interpolateArray(from.values, to.values, t));
4271
4245
  }
4272
4246
  };
4273
- //#endregion
4247
+
4274
4248
  //#region src/expression/types/color_array.ts
4275
4249
  /**
4276
4250
  * An array of colors. Create instances from
@@ -4313,7 +4287,7 @@ var ColorArray = class ColorArray {
4313
4287
  return new ColorArray(colors);
4314
4288
  }
4315
4289
  };
4316
- //#endregion
4290
+
4317
4291
  //#region src/expression/runtime_error.ts
4318
4292
  var RuntimeError = class extends Error {
4319
4293
  constructor(message, path) {
@@ -4325,7 +4299,7 @@ var RuntimeError = class extends Error {
4325
4299
  return this.message;
4326
4300
  }
4327
4301
  };
4328
- //#endregion
4302
+
4329
4303
  //#region src/expression/types/variable_anchor_offset_collection.ts
4330
4304
  /** Set of valid anchor positions, as a set for validation */
4331
4305
  const anchors = /* @__PURE__ */ new Set([
@@ -4377,7 +4351,7 @@ var VariableAnchorOffsetCollection = class VariableAnchorOffsetCollection {
4377
4351
  return new VariableAnchorOffsetCollection(output);
4378
4352
  }
4379
4353
  };
4380
- //#endregion
4354
+
4381
4355
  //#region src/expression/types/resolved_image.ts
4382
4356
  var ResolvedImage = class ResolvedImage {
4383
4357
  constructor(options) {
@@ -4395,7 +4369,7 @@ var ResolvedImage = class ResolvedImage {
4395
4369
  });
4396
4370
  }
4397
4371
  };
4398
- //#endregion
4372
+
4399
4373
  //#region src/expression/types/projection_definition.ts
4400
4374
  var ProjectionDefinition = class ProjectionDefinition {
4401
4375
  constructor(from, to, transition) {
@@ -4421,7 +4395,26 @@ var ProjectionDefinition = class ProjectionDefinition {
4421
4395
  if (typeof input === "string") return new ProjectionDefinition(input, input, 1);
4422
4396
  }
4423
4397
  };
4424
- //#endregion
4398
+
4399
+ //#region src/expression/types/collator.ts
4400
+ var Collator = class {
4401
+ constructor(caseSensitive, diacriticSensitive, locale) {
4402
+ if (caseSensitive) this.sensitivity = diacriticSensitive ? "variant" : "case";
4403
+ else this.sensitivity = diacriticSensitive ? "accent" : "base";
4404
+ this.locale = locale;
4405
+ this.collator = new Intl.Collator(this.locale ? this.locale : [], {
4406
+ sensitivity: this.sensitivity,
4407
+ usage: "search"
4408
+ });
4409
+ }
4410
+ compare(lhs, rhs) {
4411
+ return this.collator.compare(lhs, rhs);
4412
+ }
4413
+ resolvedLocale() {
4414
+ return new Intl.Collator(this.locale ? this.locale : []).resolvedOptions().locale;
4415
+ }
4416
+ };
4417
+
4425
4418
  //#region src/expression/values.ts
4426
4419
  function validateRGBA(r, g, b, a) {
4427
4420
  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" ? [
@@ -4488,7 +4481,7 @@ function valueToString(value) {
4488
4481
  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();
4489
4482
  else return JSON.stringify(value);
4490
4483
  }
4491
- //#endregion
4484
+
4492
4485
  //#region src/expression/definitions/literal.ts
4493
4486
  var Literal = class Literal {
4494
4487
  constructor(type, value) {
@@ -4497,7 +4490,7 @@ var Literal = class Literal {
4497
4490
  }
4498
4491
  static parse(args, context) {
4499
4492
  if (args.length !== 2) return context.error(`'literal' expression requires exactly one argument, but found ${args.length - 1} instead.`);
4500
- if (!isValue(args[1])) return context.error("invalid value");
4493
+ if (!isValue(args[1])) return context.error(`invalid value of type "${typeof args[1]}"`);
4501
4494
  const value = args[1];
4502
4495
  let type = typeOf(value);
4503
4496
  const expected = context.expectedType;
@@ -4512,85 +4505,524 @@ var Literal = class Literal {
4512
4505
  return true;
4513
4506
  }
4514
4507
  };
4515
- //#endregion
4516
- //#region src/expression/definitions/assertion.ts
4517
- const types$1 = {
4518
- string: StringType,
4519
- number: NumberType,
4520
- boolean: BooleanType,
4521
- object: ObjectType
4522
- };
4523
- var Assertion = class Assertion {
4524
- constructor(type, args, key) {
4525
- this.type = type;
4526
- this.args = args;
4527
- this.key = key;
4508
+
4509
+ //#region src/expression/evaluation_context.ts
4510
+ const geometryTypes = [
4511
+ "Unknown",
4512
+ "Point",
4513
+ "LineString",
4514
+ "Polygon"
4515
+ ];
4516
+ var EvaluationContext = class {
4517
+ constructor() {
4518
+ this.globals = null;
4519
+ this.feature = null;
4520
+ this.featureState = null;
4521
+ this.formattedSection = null;
4522
+ this._parseColorCache = /* @__PURE__ */ new Map();
4523
+ this.availableImages = null;
4524
+ this.canonical = null;
4528
4525
  }
4529
- static parse(args, context) {
4530
- if (args.length < 2) return context.error("Expected at least one argument.");
4531
- let i = 1;
4532
- let type;
4533
- const name = args[0];
4534
- if (name === "array") {
4535
- let itemType;
4536
- if (args.length > 2) {
4537
- const type = args[1];
4538
- 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);
4539
- itemType = types$1[type];
4540
- i++;
4541
- } else itemType = ValueType;
4542
- let N;
4543
- if (args.length > 3) {
4544
- 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);
4545
- N = args[2];
4546
- i++;
4547
- }
4548
- type = array(itemType, N);
4549
- } else {
4550
- if (!types$1[name]) throw new Error(`Types doesn't contain name = ${name}`);
4551
- type = types$1[name];
4552
- }
4553
- const parsed = [];
4554
- for (; i < args.length; i++) {
4555
- const input = context.parse(args[i], i, ValueType);
4556
- if (!input) return null;
4557
- parsed.push(input);
4558
- }
4559
- return new Assertion(type, parsed, context.key);
4526
+ id() {
4527
+ return this.feature && "id" in this.feature ? this.feature.id : null;
4560
4528
  }
4561
- evaluate(ctx) {
4562
- for (let i = 0; i < this.args.length; i++) {
4563
- const value = this.args[i].evaluate(ctx);
4564
- if (!checkSubtype(this.type, typeOf(value))) return value;
4565
- 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);
4566
- }
4567
- throw new Error();
4529
+ geometryType() {
4530
+ return this.feature ? typeof this.feature.type === "number" ? geometryTypes[this.feature.type] : this.feature.type : null;
4568
4531
  }
4569
- eachChild(fn) {
4570
- this.args.forEach(fn);
4532
+ geometry() {
4533
+ return this.feature && "geometry" in this.feature ? this.feature.geometry : null;
4571
4534
  }
4572
- outputDefined() {
4573
- return this.args.every((arg) => arg.outputDefined());
4535
+ canonicalID() {
4536
+ return this.canonical;
4537
+ }
4538
+ properties() {
4539
+ return this.feature && this.feature.properties || {};
4540
+ }
4541
+ parseColor(input) {
4542
+ let cached = this._parseColorCache.get(input);
4543
+ if (!cached) {
4544
+ cached = Color$1.parse(input);
4545
+ this._parseColorCache.set(input, cached);
4546
+ }
4547
+ return cached;
4574
4548
  }
4575
4549
  };
4576
- //#endregion
4577
- //#region src/expression/definitions/coercion.ts
4578
- const types = {
4579
- "to-boolean": BooleanType,
4580
- "to-color": ColorType,
4581
- "to-number": NumberType,
4582
- "to-string": StringType
4583
- };
4550
+
4551
+ //#region src/expression/stops.ts
4584
4552
  /**
4585
- * Special form for error-coalescing coercion expressions "to-number",
4586
- * "to-color". Since these coercions can fail at runtime, they accept multiple
4587
- * arguments, only evaluating one at a time until one succeeds.
4588
- *
4553
+ * Returns the index of the last stop <= input, or 0 if it doesn't exist.
4589
4554
  * @private
4590
4555
  */
4591
- var Coercion = class Coercion {
4592
- constructor(type, args, key) {
4593
- this.type = type;
4556
+ function findStopLessThanOrEqualTo(stops, input, key) {
4557
+ const lastIndex = stops.length - 1;
4558
+ let lowerIndex = 0;
4559
+ let upperIndex = lastIndex;
4560
+ let currentIndex = 0;
4561
+ let currentValue, nextValue;
4562
+ while (lowerIndex <= upperIndex) {
4563
+ currentIndex = Math.floor((lowerIndex + upperIndex) / 2);
4564
+ currentValue = stops[currentIndex];
4565
+ nextValue = stops[currentIndex + 1];
4566
+ if (currentValue <= input) {
4567
+ if (currentIndex === lastIndex || input < nextValue) return currentIndex;
4568
+ lowerIndex = currentIndex + 1;
4569
+ } else if (currentValue > input) upperIndex = currentIndex - 1;
4570
+ else throw new RuntimeError("Input is not a number.", key);
4571
+ }
4572
+ return 0;
4573
+ }
4574
+
4575
+ //#region src/expression/definitions/step.ts
4576
+ var Step = class Step {
4577
+ constructor(type, input, stops, key) {
4578
+ this.type = type;
4579
+ this.input = input;
4580
+ this.key = key;
4581
+ this.labels = [];
4582
+ this.outputs = [];
4583
+ for (const [label, expression] of stops) {
4584
+ this.labels.push(label);
4585
+ this.outputs.push(expression);
4586
+ }
4587
+ }
4588
+ static parse(args, context) {
4589
+ if (args.length - 1 < 4) return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`);
4590
+ if ((args.length - 1) % 2 !== 0) return context.error("Expected an even number of arguments.");
4591
+ const input = context.parse(args[1], 1, NumberType);
4592
+ if (!input) return null;
4593
+ const stops = [];
4594
+ let outputType = null;
4595
+ if (context.expectedType && context.expectedType.kind !== "value") outputType = context.expectedType;
4596
+ for (let i = 1; i < args.length; i += 2) {
4597
+ const label = i === 1 ? -Infinity : args[i];
4598
+ const value = args[i + 1];
4599
+ const labelKey = i;
4600
+ const valueKey = i + 1;
4601
+ 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);
4602
+ 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);
4603
+ const parsed = context.parse(value, valueKey, outputType);
4604
+ if (!parsed) return null;
4605
+ outputType = outputType || parsed.type;
4606
+ stops.push([label, parsed]);
4607
+ }
4608
+ return new Step(outputType, input, stops, context.key);
4609
+ }
4610
+ evaluate(ctx) {
4611
+ const labels = this.labels;
4612
+ const outputs = this.outputs;
4613
+ if (labels.length === 1) return outputs[0].evaluate(ctx);
4614
+ const value = this.input.evaluate(ctx);
4615
+ if (value <= labels[0]) return outputs[0].evaluate(ctx);
4616
+ const stopCount = labels.length;
4617
+ if (value >= labels[stopCount - 1]) return outputs[stopCount - 1].evaluate(ctx);
4618
+ return outputs[findStopLessThanOrEqualTo(labels, value, this.key)].evaluate(ctx);
4619
+ }
4620
+ eachChild(fn) {
4621
+ fn(this.input);
4622
+ for (const expression of this.outputs) fn(expression);
4623
+ }
4624
+ outputDefined() {
4625
+ return this.outputs.every((out) => out.outputDefined());
4626
+ }
4627
+ };
4628
+
4629
+ //#region node_modules/@mapbox/unitbezier/index.js
4630
+ function unitBezier(p1x, p1y, p2x, p2y) {
4631
+ const cx = 3 * p1x;
4632
+ const bx = 3 * (p2x - p1x) - cx;
4633
+ const ax = 1 - cx - bx;
4634
+ const cy = 3 * p1y;
4635
+ const by = 3 * (p2y - p1y) - cy;
4636
+ const ay = 1 - cy - by;
4637
+ return function solve(x, epsilon = 1e-6) {
4638
+ if (x <= 0) return 0;
4639
+ if (x >= 1) return 1;
4640
+ let t = x;
4641
+ for (let i = 0; i < 8; i++) {
4642
+ const x2 = ((ax * t + bx) * t + cx) * t - x;
4643
+ if (Math.abs(x2) < epsilon) return ((ay * t + by) * t + cy) * t;
4644
+ const d2 = (3 * ax * t + 2 * bx) * t + cx;
4645
+ if (Math.abs(d2) < 1e-6) break;
4646
+ t -= x2 / d2;
4647
+ }
4648
+ let t0 = 0;
4649
+ let t1 = 1;
4650
+ t = x;
4651
+ for (let i = 0; i < 20; i++) {
4652
+ const x2 = ((ax * t + bx) * t + cx) * t;
4653
+ if (Math.abs(x2 - x) < epsilon) break;
4654
+ if (x > x2) t0 = t;
4655
+ else t1 = t;
4656
+ t = (t0 + t1) * .5;
4657
+ }
4658
+ return ((ay * t + by) * t + cy) * t;
4659
+ };
4660
+ }
4661
+
4662
+ //#region src/expression/definitions/interpolate.ts
4663
+ var Interpolate = class Interpolate {
4664
+ constructor(type, operator, interpolation, input, stops, key) {
4665
+ this.type = type;
4666
+ this.operator = operator;
4667
+ this.interpolation = interpolation;
4668
+ this.input = input;
4669
+ this.key = key;
4670
+ this.labels = [];
4671
+ this.outputs = [];
4672
+ for (const [label, expression] of stops) {
4673
+ this.labels.push(label);
4674
+ this.outputs.push(expression);
4675
+ }
4676
+ }
4677
+ static interpolationFactor(interpolation, input, lower, upper) {
4678
+ let t = 0;
4679
+ if (interpolation.name === "exponential") t = exponentialInterpolation(input, interpolation.base, lower, upper);
4680
+ else if (interpolation.name === "linear") t = exponentialInterpolation(input, 1, lower, upper);
4681
+ else if (interpolation.name === "cubic-bezier") {
4682
+ const c = interpolation.controlPoints;
4683
+ t = unitBezier(c[0], c[1], c[2], c[3])(exponentialInterpolation(input, 1, lower, upper));
4684
+ }
4685
+ return t;
4686
+ }
4687
+ static parse(args, context) {
4688
+ let [operator, interpolation, input, ...rest] = args;
4689
+ if (!Array.isArray(interpolation) || interpolation.length === 0) return context.error("Expected an interpolation type expression.", 1);
4690
+ if (interpolation[0] === "linear") interpolation = { name: "linear" };
4691
+ else if (interpolation[0] === "exponential") {
4692
+ const base = interpolation[1];
4693
+ if (typeof base !== "number") return context.error("Exponential interpolation requires a numeric base.", 1, 1);
4694
+ interpolation = {
4695
+ name: "exponential",
4696
+ base
4697
+ };
4698
+ } else if (interpolation[0] === "cubic-bezier") {
4699
+ const controlPoints = interpolation.slice(1);
4700
+ 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);
4701
+ interpolation = {
4702
+ name: "cubic-bezier",
4703
+ controlPoints
4704
+ };
4705
+ } else return context.error(`Unknown interpolation type ${String(interpolation[0])}`, 1, 0);
4706
+ if (args.length - 1 < 4) return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`);
4707
+ if ((args.length - 1) % 2 !== 0) return context.error("Expected an even number of arguments.");
4708
+ input = context.parse(input, 2, NumberType);
4709
+ if (!input) return null;
4710
+ const stops = [];
4711
+ let outputType = null;
4712
+ if ((operator === "interpolate-hcl" || operator === "interpolate-lab") && context.expectedType != ColorArrayType) outputType = ColorType;
4713
+ else if (context.expectedType && context.expectedType.kind !== "value") outputType = context.expectedType;
4714
+ for (let i = 0; i < rest.length; i += 2) {
4715
+ const label = rest[i];
4716
+ const value = rest[i + 1];
4717
+ const labelKey = i + 3;
4718
+ const valueKey = i + 4;
4719
+ 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);
4720
+ 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);
4721
+ const parsed = context.parse(value, valueKey, outputType);
4722
+ if (!parsed) return null;
4723
+ outputType = outputType || parsed.type;
4724
+ stops.push([label, parsed]);
4725
+ }
4726
+ 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.`);
4727
+ return new Interpolate(outputType, operator, interpolation, input, stops, context.key);
4728
+ }
4729
+ evaluate(ctx) {
4730
+ const labels = this.labels;
4731
+ const outputs = this.outputs;
4732
+ if (labels.length === 1) return outputs[0].evaluate(ctx);
4733
+ const value = this.input.evaluate(ctx);
4734
+ if (value <= labels[0]) return outputs[0].evaluate(ctx);
4735
+ const stopCount = labels.length;
4736
+ if (value >= labels[stopCount - 1]) return outputs[stopCount - 1].evaluate(ctx);
4737
+ const index = findStopLessThanOrEqualTo(labels, value, this.key);
4738
+ const lower = labels[index];
4739
+ const upper = labels[index + 1];
4740
+ const t = Interpolate.interpolationFactor(this.interpolation, value, lower, upper);
4741
+ const outputLower = outputs[index].evaluate(ctx);
4742
+ const outputUpper = outputs[index + 1].evaluate(ctx);
4743
+ switch (this.operator) {
4744
+ case "interpolate": switch (this.type.kind) {
4745
+ case "number": return interpolateNumber(outputLower, outputUpper, t);
4746
+ case "color": return Color$1.interpolate(outputLower, outputUpper, t);
4747
+ case "padding": return Padding.interpolate(outputLower, outputUpper, t);
4748
+ case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t);
4749
+ case "numberArray": return NumberArray.interpolate(outputLower, outputUpper, t);
4750
+ case "variableAnchorOffsetCollection": return VariableAnchorOffsetCollection.interpolate(outputLower, outputUpper, t, this.key);
4751
+ case "array": return interpolateArray(outputLower, outputUpper, t);
4752
+ case "projectionDefinition": return ProjectionDefinition.interpolate(outputLower, outputUpper, t);
4753
+ }
4754
+ case "interpolate-hcl": switch (this.type.kind) {
4755
+ case "color": return Color$1.interpolate(outputLower, outputUpper, t, "hcl");
4756
+ case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t, "hcl");
4757
+ }
4758
+ case "interpolate-lab": switch (this.type.kind) {
4759
+ case "color": return Color$1.interpolate(outputLower, outputUpper, t, "lab");
4760
+ case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t, "lab");
4761
+ }
4762
+ }
4763
+ }
4764
+ eachChild(fn) {
4765
+ fn(this.input);
4766
+ for (const expression of this.outputs) fn(expression);
4767
+ }
4768
+ outputDefined() {
4769
+ return this.outputs.every((out) => out.outputDefined());
4770
+ }
4771
+ };
4772
+ /**
4773
+ * Returns a ratio that can be used to interpolate between exponential function
4774
+ * stops.
4775
+ * 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,
4776
+ * and `a` and `b` are constants affording sufficient degrees of freedom to fit
4777
+ * the function to the given stops.
4778
+ *
4779
+ * Here's a bit of algebra that lets us compute `f(x)` directly from the stop
4780
+ * values without explicitly solving for `a` and `b`:
4781
+ *
4782
+ * First stop value: `f(x0) = y0 = a * base^x0 + b`
4783
+ * Second stop value: `f(x1) = y1 = a * base^x1 + b`
4784
+ * => `y1 - y0 = a(base^x1 - base^x0)`
4785
+ * => `a = (y1 - y0)/(base^x1 - base^x0)`
4786
+ *
4787
+ * Desired value: `f(x) = y = a * base^x + b`
4788
+ * => `f(x) = y0 + a * (base^x - base^x0)`
4789
+ *
4790
+ * From the above, we can replace the `a` in `a * (base^x - base^x0)` and do a
4791
+ * little algebra:
4792
+ * ```
4793
+ * a * (base^x - base^x0) = (y1 - y0)/(base^x1 - base^x0) * (base^x - base^x0)
4794
+ * = (y1 - y0) * (base^x - base^x0) / (base^x1 - base^x0)
4795
+ * ```
4796
+ *
4797
+ * If we let `(base^x - base^x0) / (base^x1 base^x0)`, then we have
4798
+ * `f(x) = y0 + (y1 - y0) * ratio`. In other words, `ratio` may be treated as
4799
+ * an interpolation factor between the two stops' output values.
4800
+ *
4801
+ * (Note: a slightly different form for `ratio`,
4802
+ * `(base^(x-x0) - 1) / (base^(x1-x0) - 1) `, is equivalent, but requires fewer
4803
+ * expensive `Math.pow()` operations.)
4804
+ *
4805
+ * @private
4806
+ */
4807
+ function exponentialInterpolation(input, base, lowerValue, upperValue) {
4808
+ const difference = upperValue - lowerValue;
4809
+ const progress = input - lowerValue;
4810
+ if (difference === 0) return 0;
4811
+ else if (base === 1) return progress / difference;
4812
+ else return (Math.pow(base, progress) - 1) / (Math.pow(base, difference) - 1);
4813
+ }
4814
+ const interpolateFactory = {
4815
+ color: Color$1.interpolate,
4816
+ number: interpolateNumber,
4817
+ padding: Padding.interpolate,
4818
+ numberArray: NumberArray.interpolate,
4819
+ colorArray: ColorArray.interpolate,
4820
+ variableAnchorOffsetCollection: VariableAnchorOffsetCollection.interpolate,
4821
+ array: interpolateArray
4822
+ };
4823
+
4824
+ //#region src/expression/definitions/format.ts
4825
+ var FormatExpression = class FormatExpression {
4826
+ constructor(sections) {
4827
+ this.type = FormattedType;
4828
+ this.sections = sections;
4829
+ }
4830
+ static parse(args, context) {
4831
+ if (args.length < 2) return context.error("Expected at least one argument.");
4832
+ const firstArg = args[1];
4833
+ if (!Array.isArray(firstArg) && typeof firstArg === "object") return context.error("First argument must be an image or text section.");
4834
+ const sections = [];
4835
+ let nextTokenMayBeObject = false;
4836
+ for (let i = 1; i <= args.length - 1; ++i) {
4837
+ const arg = args[i];
4838
+ if (nextTokenMayBeObject && typeof arg === "object" && !Array.isArray(arg)) {
4839
+ nextTokenMayBeObject = false;
4840
+ let scale = null;
4841
+ if (arg["font-scale"]) {
4842
+ scale = context.parse(arg["font-scale"], 1, NumberType);
4843
+ if (!scale) return null;
4844
+ }
4845
+ let font = null;
4846
+ if (arg["text-font"]) {
4847
+ font = context.parse(arg["text-font"], 1, array(StringType));
4848
+ if (!font) return null;
4849
+ }
4850
+ let textColor = null;
4851
+ if (arg["text-color"]) {
4852
+ textColor = context.parse(arg["text-color"], 1, ColorType);
4853
+ if (!textColor) return null;
4854
+ }
4855
+ let verticalAlign = null;
4856
+ if (arg["vertical-align"]) {
4857
+ 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.`);
4858
+ verticalAlign = context.parse(arg["vertical-align"], 1, StringType);
4859
+ if (!verticalAlign) return null;
4860
+ }
4861
+ const lastExpression = sections[sections.length - 1];
4862
+ lastExpression.scale = scale;
4863
+ lastExpression.font = font;
4864
+ lastExpression.textColor = textColor;
4865
+ lastExpression.verticalAlign = verticalAlign;
4866
+ } else {
4867
+ const content = context.parse(args[i], 1, ValueType);
4868
+ if (!content) return null;
4869
+ const kind = content.type.kind;
4870
+ if (kind !== "string" && kind !== "value" && kind !== "null" && kind !== "resolvedImage") return context.error("Formatted text type must be 'string', 'value', 'image' or 'null'.");
4871
+ nextTokenMayBeObject = true;
4872
+ sections.push({
4873
+ content,
4874
+ scale: null,
4875
+ font: null,
4876
+ textColor: null,
4877
+ verticalAlign: null
4878
+ });
4879
+ }
4880
+ }
4881
+ return new FormatExpression(sections);
4882
+ }
4883
+ evaluate(ctx) {
4884
+ const evaluateSection = (section) => {
4885
+ const evaluatedContent = section.content.evaluate(ctx);
4886
+ if (typeOf(evaluatedContent) === ResolvedImageType) return new FormattedSection("", evaluatedContent, null, null, null, section.verticalAlign ? section.verticalAlign.evaluate(ctx) : null);
4887
+ 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);
4888
+ };
4889
+ return new Formatted(this.sections.map(evaluateSection));
4890
+ }
4891
+ eachChild(fn) {
4892
+ for (const section of this.sections) {
4893
+ fn(section.content);
4894
+ if (section.scale) fn(section.scale);
4895
+ if (section.font) fn(section.font);
4896
+ if (section.textColor) fn(section.textColor);
4897
+ if (section.verticalAlign) fn(section.verticalAlign);
4898
+ }
4899
+ }
4900
+ outputDefined() {
4901
+ return false;
4902
+ }
4903
+ };
4904
+
4905
+ //#region src/util/classify_rings.ts
4906
+ /**
4907
+ * Classifies an array of rings into polygons with outer rings and holes
4908
+ * @param rings - the rings to classify
4909
+ * @param maxRings - the maximum number of rings to include in a polygon, use 0 to include all rings
4910
+ * @returns an array of polygons with internal rings as holes
4911
+ */
4912
+ function classifyRings$1(rings, maxRings) {
4913
+ if (rings.length <= 1) return [rings];
4914
+ const polygons = [];
4915
+ let polygon;
4916
+ let ccw;
4917
+ for (const ring of rings) {
4918
+ const area = calculateSignedArea(ring);
4919
+ if (area === 0) continue;
4920
+ ring.area = Math.abs(area);
4921
+ if (ccw === void 0) ccw = area < 0;
4922
+ if (ccw === area < 0) {
4923
+ if (polygon) polygons.push(polygon);
4924
+ polygon = [ring];
4925
+ } else polygon.push(ring);
4926
+ }
4927
+ if (polygon) polygons.push(polygon);
4928
+ return polygons;
4929
+ }
4930
+ /**
4931
+ * Returns the signed area for the polygon ring. Positive areas are exterior rings and
4932
+ * have a clockwise winding. Negative areas are interior rings and have a counter clockwise
4933
+ * ordering.
4934
+ *
4935
+ * @param ring - Exterior or interior ring
4936
+ * @returns Signed area
4937
+ */
4938
+ function calculateSignedArea(ring) {
4939
+ let sum = 0;
4940
+ for (let i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) {
4941
+ p1 = ring[i];
4942
+ p2 = ring[j];
4943
+ sum += (p2.x - p1.x) * (p1.y + p2.y);
4944
+ }
4945
+ return sum;
4946
+ }
4947
+
4948
+ //#region src/expression/definitions/assertion.ts
4949
+ const types$1 = {
4950
+ string: StringType,
4951
+ number: NumberType,
4952
+ boolean: BooleanType,
4953
+ object: ObjectType
4954
+ };
4955
+ var Assertion = class Assertion {
4956
+ constructor(type, args, key) {
4957
+ this.type = type;
4958
+ this.args = args;
4959
+ this.key = key;
4960
+ }
4961
+ static parse(args, context) {
4962
+ if (args.length < 2) return context.error("Expected at least one argument.");
4963
+ let i = 1;
4964
+ let type;
4965
+ const name = args[0];
4966
+ if (name === "array") {
4967
+ let itemType;
4968
+ if (args.length > 2) {
4969
+ const type = args[1];
4970
+ 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);
4971
+ itemType = types$1[type];
4972
+ i++;
4973
+ } else itemType = ValueType;
4974
+ let N;
4975
+ if (args.length > 3) {
4976
+ 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);
4977
+ N = args[2];
4978
+ i++;
4979
+ }
4980
+ type = array(itemType, N);
4981
+ } else {
4982
+ if (!types$1[name]) throw new Error(`Types doesn't contain name = ${name}`);
4983
+ type = types$1[name];
4984
+ }
4985
+ const parsed = [];
4986
+ for (; i < args.length; i++) {
4987
+ const input = context.parse(args[i], i, ValueType);
4988
+ if (!input) return null;
4989
+ parsed.push(input);
4990
+ }
4991
+ return new Assertion(type, parsed, context.key);
4992
+ }
4993
+ evaluate(ctx) {
4994
+ for (let i = 0; i < this.args.length; i++) {
4995
+ const value = this.args[i].evaluate(ctx);
4996
+ if (!checkSubtype(this.type, typeOf(value))) return value;
4997
+ 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);
4998
+ }
4999
+ throw new Error();
5000
+ }
5001
+ eachChild(fn) {
5002
+ this.args.forEach(fn);
5003
+ }
5004
+ outputDefined() {
5005
+ return this.args.every((arg) => arg.outputDefined());
5006
+ }
5007
+ };
5008
+
5009
+ //#region src/expression/definitions/coercion.ts
5010
+ const types = {
5011
+ "to-boolean": BooleanType,
5012
+ "to-color": ColorType,
5013
+ "to-number": NumberType,
5014
+ "to-string": StringType
5015
+ };
5016
+ /**
5017
+ * Special form for error-coalescing coercion expressions "to-number",
5018
+ * "to-color". Since these coercions can fail at runtime, they accept multiple
5019
+ * arguments, only evaluating one at a time until one succeeds.
5020
+ *
5021
+ * @private
5022
+ */
5023
+ var Coercion = class Coercion {
5024
+ constructor(type, args, key) {
5025
+ this.type = type;
4594
5026
  this.args = args;
4595
5027
  this.key = key;
4596
5028
  }
@@ -4653,203 +5085,47 @@ var Coercion = class Coercion {
4653
5085
  input = arg.evaluate(ctx);
4654
5086
  const val = ColorArray.parse(input);
4655
5087
  if (val) return val;
4656
- }
4657
- throw new RuntimeError(`Could not parse colorArray from value '${typeof input === "string" ? input : JSON.stringify(input)}'`, this.key);
4658
- }
4659
- case "variableAnchorOffsetCollection": {
4660
- let input;
4661
- for (const arg of this.args) {
4662
- input = arg.evaluate(ctx);
4663
- const coll = VariableAnchorOffsetCollection.parse(input);
4664
- if (coll) return coll;
4665
- }
4666
- throw new RuntimeError(`Could not parse variableAnchorOffsetCollection from value '${typeof input === "string" ? input : JSON.stringify(input)}'`, this.key);
4667
- }
4668
- case "number": {
4669
- let value = null;
4670
- for (const arg of this.args) {
4671
- value = arg.evaluate(ctx);
4672
- if (value === null) return 0;
4673
- const num = Number(value);
4674
- if (isNaN(num)) continue;
4675
- return num;
4676
- }
4677
- throw new RuntimeError(`Could not convert ${JSON.stringify(value)} to number.`, this.key);
4678
- }
4679
- case "formatted": return Formatted.fromString(valueToString(this.args[0].evaluate(ctx)));
4680
- case "resolvedImage": return ResolvedImage.fromString(valueToString(this.args[0].evaluate(ctx)));
4681
- case "projectionDefinition": return this.args[0].evaluate(ctx);
4682
- default: return valueToString(this.args[0].evaluate(ctx));
4683
- }
4684
- }
4685
- eachChild(fn) {
4686
- this.args.forEach(fn);
4687
- }
4688
- outputDefined() {
4689
- return this.args.every((arg) => arg.outputDefined());
4690
- }
4691
- };
4692
- //#endregion
4693
- //#region src/expression/evaluation_context.ts
4694
- const geometryTypes = [
4695
- "Unknown",
4696
- "Point",
4697
- "LineString",
4698
- "Polygon"
4699
- ];
4700
- var EvaluationContext = class {
4701
- constructor() {
4702
- this.globals = null;
4703
- this.feature = null;
4704
- this.featureState = null;
4705
- this.formattedSection = null;
4706
- this._parseColorCache = /* @__PURE__ */ new Map();
4707
- this.availableImages = null;
4708
- this.canonical = null;
4709
- }
4710
- id() {
4711
- return this.feature && "id" in this.feature ? this.feature.id : null;
4712
- }
4713
- geometryType() {
4714
- return this.feature ? typeof this.feature.type === "number" ? geometryTypes[this.feature.type] : this.feature.type : null;
4715
- }
4716
- geometry() {
4717
- return this.feature && "geometry" in this.feature ? this.feature.geometry : null;
4718
- }
4719
- canonicalID() {
4720
- return this.canonical;
4721
- }
4722
- properties() {
4723
- return this.feature && this.feature.properties || {};
4724
- }
4725
- parseColor(input) {
4726
- let cached = this._parseColorCache.get(input);
4727
- if (!cached) {
4728
- cached = Color$1.parse(input);
4729
- this._parseColorCache.set(input, cached);
4730
- }
4731
- return cached;
4732
- }
4733
- };
4734
- //#endregion
4735
- //#region src/expression/parsing_context.ts
4736
- /**
4737
- * State associated parsing at a given point in an expression tree.
4738
- * @private
4739
- */
4740
- var ParsingContext = class ParsingContext {
4741
- constructor(registry, isConstantFunc, path = [], expectedType, scope = new Scope(), errors = []) {
4742
- this.registry = registry;
4743
- this.path = path;
4744
- this.key = path.map((part) => `[${part}]`).join("");
4745
- this.scope = scope;
4746
- this.errors = errors;
4747
- this.expectedType = expectedType;
4748
- this._isConstant = isConstantFunc;
4749
- }
4750
- /**
4751
- * @param expr the JSON expression to parse
4752
- * @param index the optional argument index if this expression is an argument of a parent expression that's being parsed
4753
- * @param options
4754
- * @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.
4755
- * @private
4756
- */
4757
- parse(expr, index, expectedType, bindings, options = {}) {
4758
- if (index) return this.concat(index, expectedType, bindings)._parse(expr, options);
4759
- return this._parse(expr, options);
4760
- }
4761
- _parse(expr, options) {
4762
- if (expr === null || typeof expr === "string" || typeof expr === "boolean" || typeof expr === "number") expr = ["literal", expr];
4763
- const key = this.key;
4764
- function annotate(parsed, type, typeAnnotation) {
4765
- if (typeAnnotation === "assert") return new Assertion(type, [parsed], key);
4766
- else if (typeAnnotation === "coerce") return new Coercion(type, [parsed], key);
4767
- else return parsed;
4768
- }
4769
- if (Array.isArray(expr)) {
4770
- if (expr.length === 0) return this.error("Expected an array with at least one element. If you wanted a literal array, use [\"literal\", []].");
4771
- const op = expr[0];
4772
- if (typeof op !== "string") {
4773
- this.error(`Expression name must be a string, but found ${typeof op} instead. If you wanted a literal array, use ["literal", [...]].`, 0);
4774
- return null;
5088
+ }
5089
+ throw new RuntimeError(`Could not parse colorArray from value '${typeof input === "string" ? input : JSON.stringify(input)}'`, this.key);
4775
5090
  }
4776
- const Expr = this.registry[op];
4777
- if (Expr) {
4778
- let parsed = Expr.parse(expr, this);
4779
- if (!parsed) return null;
4780
- if (this.expectedType) {
4781
- const expected = this.expectedType;
4782
- const actual = parsed.type;
4783
- 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");
4784
- else if ("projectionDefinition" === expected.kind && ["string", "array"].includes(actual.kind) || [
4785
- "color",
4786
- "formatted",
4787
- "resolvedImage"
4788
- ].includes(expected.kind) && ["value", "string"].includes(actual.kind) || ["padding", "numberArray"].includes(expected.kind) && [
4789
- "value",
4790
- "number",
4791
- "array"
4792
- ].includes(actual.kind) || "colorArray" === expected.kind && [
4793
- "value",
4794
- "string",
4795
- "array"
4796
- ].includes(actual.kind) || "variableAnchorOffsetCollection" === expected.kind && ["value", "array"].includes(actual.kind)) parsed = annotate(parsed, expected, options.typeAnnotation || "coerce");
4797
- else if (this.checkSubtype(expected, actual)) return null;
5091
+ case "variableAnchorOffsetCollection": {
5092
+ let input;
5093
+ for (const arg of this.args) {
5094
+ input = arg.evaluate(ctx);
5095
+ const coll = VariableAnchorOffsetCollection.parse(input);
5096
+ if (coll) return coll;
4798
5097
  }
4799
- if (!(parsed instanceof Literal) && parsed.type.kind !== "resolvedImage" && this._isConstant(parsed)) {
4800
- const ec = new EvaluationContext();
4801
- try {
4802
- parsed = new Literal(parsed.type, parsed.evaluate(ec));
4803
- } catch (e) {
4804
- this.error(e.message);
4805
- return null;
4806
- }
5098
+ throw new RuntimeError(`Could not parse variableAnchorOffsetCollection from value '${typeof input === "string" ? input : JSON.stringify(input)}'`, this.key);
5099
+ }
5100
+ case "number": {
5101
+ let value = null;
5102
+ for (const arg of this.args) {
5103
+ value = arg.evaluate(ctx);
5104
+ if (value === null) return 0;
5105
+ const num = Number(value);
5106
+ if (isNaN(num)) continue;
5107
+ return num;
4807
5108
  }
4808
- return parsed;
5109
+ throw new RuntimeError(`Could not convert ${JSON.stringify(value)} to number.`, this.key);
4809
5110
  }
4810
- return this.error(`Unknown expression "${op}". If you wanted a literal array, use ["literal", [...]].`, 0);
4811
- } else if (typeof expr === "undefined") return this.error("'undefined' value invalid. Use null instead.");
4812
- else if (typeof expr === "object") return this.error("Bare objects invalid. Use [\"literal\", {...}] instead.");
4813
- else return this.error(`Expected an array, but found ${typeof expr} instead.`);
4814
- }
4815
- /**
4816
- * Returns a copy of this context suitable for parsing the subexpression at
4817
- * index `index`, optionally appending to 'let' binding map.
4818
- *
4819
- * Note that `errors` property, intended for collecting errors while
4820
- * parsing, is copied by reference rather than cloned.
4821
- * @private
4822
- */
4823
- concat(index, expectedType, bindings) {
4824
- const path = typeof index === "number" ? this.path.concat(index) : this.path;
4825
- const scope = bindings ? this.scope.concat(bindings) : this.scope;
4826
- return new ParsingContext(this.registry, this._isConstant, path, expectedType || null, scope, this.errors);
5111
+ case "formatted": return Formatted.fromString(valueToString(this.args[0].evaluate(ctx)));
5112
+ case "resolvedImage": return ResolvedImage.fromString(valueToString(this.args[0].evaluate(ctx)));
5113
+ case "projectionDefinition": {
5114
+ const input = this.args[0].evaluate(ctx);
5115
+ if (ProjectionDefinition.parse(input)) return input;
5116
+ throw new RuntimeError(`Could not parse projectionDefinition from value '${typeof input === "string" ? input : JSON.stringify(input)}'`, this.key);
5117
+ }
5118
+ default: return valueToString(this.args[0].evaluate(ctx));
5119
+ }
4827
5120
  }
4828
- /**
4829
- * Push a parsing (or type checking) error into the `this.errors`
4830
- * @param error The message
4831
- * @param keys Optionally specify the source of the error at a child
4832
- * of the current expression at `this.key`.
4833
- * @private
4834
- */
4835
- error(error, ...keys) {
4836
- const key = `${this.key}${keys.map((k) => `[${k}]`).join("")}`;
4837
- this.errors.push(new ExpressionParsingError(key, error));
5121
+ eachChild(fn) {
5122
+ this.args.forEach(fn);
4838
5123
  }
4839
- /**
4840
- * Returns null if `t` is a subtype of `expected`; otherwise returns an
4841
- * error message and also pushes it to `this.errors`.
4842
- * @param expected The expected type
4843
- * @param t The actual type
4844
- * @returns null if `t` is a subtype of `expected`; otherwise returns an error message
4845
- */
4846
- checkSubtype(expected, t) {
4847
- const error = checkSubtype(expected, t);
4848
- if (error) this.error(error);
4849
- return error;
5124
+ outputDefined() {
5125
+ return this.args.every((arg) => arg.outputDefined());
4850
5126
  }
4851
5127
  };
4852
- //#endregion
5128
+
4853
5129
  //#region src/expression/definitions/let.ts
4854
5130
  var Let = class Let {
4855
5131
  constructor(bindings, result) {
@@ -4883,7 +5159,7 @@ var Let = class Let {
4883
5159
  return this.result.outputDefined();
4884
5160
  }
4885
5161
  };
4886
- //#endregion
5162
+
4887
5163
  //#region src/expression/definitions/var.ts
4888
5164
  var Var = class Var {
4889
5165
  constructor(name, boundExpression) {
@@ -4905,7 +5181,7 @@ var Var = class Var {
4905
5181
  return false;
4906
5182
  }
4907
5183
  };
4908
- //#endregion
5184
+
4909
5185
  //#region src/expression/definitions/at.ts
4910
5186
  var At = class At {
4911
5187
  constructor(type, index, input, key) {
@@ -4938,7 +5214,7 @@ var At = class At {
4938
5214
  return false;
4939
5215
  }
4940
5216
  };
4941
- //#endregion
5217
+
4942
5218
  //#region src/expression/definitions/in.ts
4943
5219
  var In = class In {
4944
5220
  constructor(needle, haystack, key) {
@@ -4982,7 +5258,7 @@ var In = class In {
4982
5258
  return true;
4983
5259
  }
4984
5260
  };
4985
- //#endregion
5261
+
4986
5262
  //#region src/expression/definitions/index_of.ts
4987
5263
  var IndexOf = class IndexOf {
4988
5264
  constructor(needle, haystack, key, fromIndex) {
@@ -5037,7 +5313,7 @@ var IndexOf = class IndexOf {
5037
5313
  return false;
5038
5314
  }
5039
5315
  };
5040
- //#endregion
5316
+
5041
5317
  //#region src/expression/definitions/match.ts
5042
5318
  var Match = class Match {
5043
5319
  constructor(inputType, outputType, input, cases, outputs, otherwise) {
@@ -5096,7 +5372,7 @@ var Match = class Match {
5096
5372
  return this.outputs.every((out) => out.outputDefined()) && this.otherwise.outputDefined();
5097
5373
  }
5098
5374
  };
5099
- //#endregion
5375
+
5100
5376
  //#region src/expression/definitions/case.ts
5101
5377
  var Case = class Case {
5102
5378
  constructor(type, branches, otherwise) {
@@ -5138,324 +5414,51 @@ var Case = class Case {
5138
5414
  return this.branches.every(([_, out]) => out.outputDefined()) && this.otherwise.outputDefined();
5139
5415
  }
5140
5416
  };
5141
- //#endregion
5417
+
5142
5418
  //#region src/expression/definitions/slice.ts
5143
5419
  var Slice = class Slice {
5144
- constructor(type, input, beginIndex, key, endIndex) {
5145
- this.type = type;
5146
- this.input = input;
5147
- this.beginIndex = beginIndex;
5148
- this.key = key;
5149
- this.endIndex = endIndex;
5150
- }
5151
- static parse(args, context) {
5152
- if (args.length <= 2 || args.length >= 5) return context.error(`Expected 2 or 3 arguments, but found ${args.length - 1} instead.`);
5153
- const input = context.parse(args[1], 1, ValueType);
5154
- const beginIndex = context.parse(args[2], 2, NumberType);
5155
- if (!input || !beginIndex) return null;
5156
- if (!isValidType(input.type, [
5157
- array(ValueType),
5158
- StringType,
5159
- ValueType
5160
- ])) return context.error(`Expected first argument to be of type array or string, but found ${typeToString(input.type)} instead`);
5161
- if (args.length === 4) {
5162
- const endIndex = context.parse(args[3], 3, NumberType);
5163
- if (!endIndex) return null;
5164
- return new Slice(input.type, input, beginIndex, context.key, endIndex);
5165
- } else return new Slice(input.type, input, beginIndex, context.key);
5166
- }
5167
- evaluate(ctx) {
5168
- const input = this.input.evaluate(ctx);
5169
- const beginIndex = this.beginIndex.evaluate(ctx);
5170
- let endIndex;
5171
- if (this.endIndex) endIndex = this.endIndex.evaluate(ctx);
5172
- if (isValidNativeType(input, ["string"])) return [...input].slice(beginIndex, endIndex).join("");
5173
- else if (isValidNativeType(input, ["array"])) return input.slice(beginIndex, endIndex);
5174
- else throw new RuntimeError(`Expected first argument to be of type array or string, but found ${typeToString(typeOf(input))} instead.`, this.key);
5175
- }
5176
- eachChild(fn) {
5177
- fn(this.input);
5178
- fn(this.beginIndex);
5179
- if (this.endIndex) fn(this.endIndex);
5180
- }
5181
- outputDefined() {
5182
- return false;
5183
- }
5184
- };
5185
- //#endregion
5186
- //#region src/expression/stops.ts
5187
- /**
5188
- * Returns the index of the last stop <= input, or 0 if it doesn't exist.
5189
- * @private
5190
- */
5191
- function findStopLessThanOrEqualTo(stops, input, key) {
5192
- const lastIndex = stops.length - 1;
5193
- let lowerIndex = 0;
5194
- let upperIndex = lastIndex;
5195
- let currentIndex = 0;
5196
- let currentValue, nextValue;
5197
- while (lowerIndex <= upperIndex) {
5198
- currentIndex = Math.floor((lowerIndex + upperIndex) / 2);
5199
- currentValue = stops[currentIndex];
5200
- nextValue = stops[currentIndex + 1];
5201
- if (currentValue <= input) {
5202
- if (currentIndex === lastIndex || input < nextValue) return currentIndex;
5203
- lowerIndex = currentIndex + 1;
5204
- } else if (currentValue > input) upperIndex = currentIndex - 1;
5205
- else throw new RuntimeError("Input is not a number.", key);
5206
- }
5207
- return 0;
5208
- }
5209
- //#endregion
5210
- //#region src/expression/definitions/step.ts
5211
- var Step = class Step {
5212
- constructor(type, input, stops, key) {
5213
- this.type = type;
5214
- this.input = input;
5215
- this.key = key;
5216
- this.labels = [];
5217
- this.outputs = [];
5218
- for (const [label, expression] of stops) {
5219
- this.labels.push(label);
5220
- this.outputs.push(expression);
5221
- }
5222
- }
5223
- static parse(args, context) {
5224
- if (args.length - 1 < 4) return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`);
5225
- if ((args.length - 1) % 2 !== 0) return context.error("Expected an even number of arguments.");
5226
- const input = context.parse(args[1], 1, NumberType);
5227
- if (!input) return null;
5228
- const stops = [];
5229
- let outputType = null;
5230
- if (context.expectedType && context.expectedType.kind !== "value") outputType = context.expectedType;
5231
- for (let i = 1; i < args.length; i += 2) {
5232
- const label = i === 1 ? -Infinity : args[i];
5233
- const value = args[i + 1];
5234
- const labelKey = i;
5235
- const valueKey = i + 1;
5236
- 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);
5237
- 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);
5238
- const parsed = context.parse(value, valueKey, outputType);
5239
- if (!parsed) return null;
5240
- outputType = outputType || parsed.type;
5241
- stops.push([label, parsed]);
5242
- }
5243
- return new Step(outputType, input, stops, context.key);
5244
- }
5245
- evaluate(ctx) {
5246
- const labels = this.labels;
5247
- const outputs = this.outputs;
5248
- if (labels.length === 1) return outputs[0].evaluate(ctx);
5249
- const value = this.input.evaluate(ctx);
5250
- if (value <= labels[0]) return outputs[0].evaluate(ctx);
5251
- const stopCount = labels.length;
5252
- if (value >= labels[stopCount - 1]) return outputs[stopCount - 1].evaluate(ctx);
5253
- return outputs[findStopLessThanOrEqualTo(labels, value, this.key)].evaluate(ctx);
5254
- }
5255
- eachChild(fn) {
5256
- fn(this.input);
5257
- for (const expression of this.outputs) fn(expression);
5258
- }
5259
- outputDefined() {
5260
- return this.outputs.every((out) => out.outputDefined());
5261
- }
5262
- };
5263
- //#endregion
5264
- //#region node_modules/@mapbox/unitbezier/index.js
5265
- function unitBezier(p1x, p1y, p2x, p2y) {
5266
- const cx = 3 * p1x;
5267
- const bx = 3 * (p2x - p1x) - cx;
5268
- const ax = 1 - cx - bx;
5269
- const cy = 3 * p1y;
5270
- const by = 3 * (p2y - p1y) - cy;
5271
- const ay = 1 - cy - by;
5272
- return function solve(x, epsilon = 1e-6) {
5273
- if (x <= 0) return 0;
5274
- if (x >= 1) return 1;
5275
- let t = x;
5276
- for (let i = 0; i < 8; i++) {
5277
- const x2 = ((ax * t + bx) * t + cx) * t - x;
5278
- if (Math.abs(x2) < epsilon) return ((ay * t + by) * t + cy) * t;
5279
- const d2 = (3 * ax * t + 2 * bx) * t + cx;
5280
- if (Math.abs(d2) < 1e-6) break;
5281
- t -= x2 / d2;
5282
- }
5283
- let t0 = 0;
5284
- let t1 = 1;
5285
- t = x;
5286
- for (let i = 0; i < 20; i++) {
5287
- const x2 = ((ax * t + bx) * t + cx) * t;
5288
- if (Math.abs(x2 - x) < epsilon) break;
5289
- if (x > x2) t0 = t;
5290
- else t1 = t;
5291
- t = (t0 + t1) * .5;
5292
- }
5293
- return ((ay * t + by) * t + cy) * t;
5294
- };
5295
- }
5296
- //#endregion
5297
- //#region src/expression/definitions/interpolate.ts
5298
- var Interpolate = class Interpolate {
5299
- constructor(type, operator, interpolation, input, stops, key) {
5420
+ constructor(type, input, beginIndex, key, endIndex) {
5300
5421
  this.type = type;
5301
- this.operator = operator;
5302
- this.interpolation = interpolation;
5303
5422
  this.input = input;
5423
+ this.beginIndex = beginIndex;
5304
5424
  this.key = key;
5305
- this.labels = [];
5306
- this.outputs = [];
5307
- for (const [label, expression] of stops) {
5308
- this.labels.push(label);
5309
- this.outputs.push(expression);
5310
- }
5311
- }
5312
- static interpolationFactor(interpolation, input, lower, upper) {
5313
- let t = 0;
5314
- if (interpolation.name === "exponential") t = exponentialInterpolation(input, interpolation.base, lower, upper);
5315
- else if (interpolation.name === "linear") t = exponentialInterpolation(input, 1, lower, upper);
5316
- else if (interpolation.name === "cubic-bezier") {
5317
- const c = interpolation.controlPoints;
5318
- t = unitBezier(c[0], c[1], c[2], c[3])(exponentialInterpolation(input, 1, lower, upper));
5319
- }
5320
- return t;
5425
+ this.endIndex = endIndex;
5321
5426
  }
5322
5427
  static parse(args, context) {
5323
- let [operator, interpolation, input, ...rest] = args;
5324
- if (!Array.isArray(interpolation) || interpolation.length === 0) return context.error("Expected an interpolation type expression.", 1);
5325
- if (interpolation[0] === "linear") interpolation = { name: "linear" };
5326
- else if (interpolation[0] === "exponential") {
5327
- const base = interpolation[1];
5328
- if (typeof base !== "number") return context.error("Exponential interpolation requires a numeric base.", 1, 1);
5329
- interpolation = {
5330
- name: "exponential",
5331
- base
5332
- };
5333
- } else if (interpolation[0] === "cubic-bezier") {
5334
- const controlPoints = interpolation.slice(1);
5335
- 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);
5336
- interpolation = {
5337
- name: "cubic-bezier",
5338
- controlPoints
5339
- };
5340
- } else return context.error(`Unknown interpolation type ${String(interpolation[0])}`, 1, 0);
5341
- if (args.length - 1 < 4) return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`);
5342
- if ((args.length - 1) % 2 !== 0) return context.error("Expected an even number of arguments.");
5343
- input = context.parse(input, 2, NumberType);
5344
- if (!input) return null;
5345
- const stops = [];
5346
- let outputType = null;
5347
- if ((operator === "interpolate-hcl" || operator === "interpolate-lab") && context.expectedType != ColorArrayType) outputType = ColorType;
5348
- else if (context.expectedType && context.expectedType.kind !== "value") outputType = context.expectedType;
5349
- for (let i = 0; i < rest.length; i += 2) {
5350
- const label = rest[i];
5351
- const value = rest[i + 1];
5352
- const labelKey = i + 3;
5353
- const valueKey = i + 4;
5354
- 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);
5355
- 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);
5356
- const parsed = context.parse(value, valueKey, outputType);
5357
- if (!parsed) return null;
5358
- outputType = outputType || parsed.type;
5359
- stops.push([label, parsed]);
5360
- }
5361
- 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.`);
5362
- return new Interpolate(outputType, operator, interpolation, input, stops, context.key);
5428
+ if (args.length <= 2 || args.length >= 5) return context.error(`Expected 2 or 3 arguments, but found ${args.length - 1} instead.`);
5429
+ const input = context.parse(args[1], 1, ValueType);
5430
+ const beginIndex = context.parse(args[2], 2, NumberType);
5431
+ if (!input || !beginIndex) return null;
5432
+ if (!isValidType(input.type, [
5433
+ array(ValueType),
5434
+ StringType,
5435
+ ValueType
5436
+ ])) return context.error(`Expected first argument to be of type array or string, but found ${typeToString(input.type)} instead`);
5437
+ if (args.length === 4) {
5438
+ const endIndex = context.parse(args[3], 3, NumberType);
5439
+ if (!endIndex) return null;
5440
+ return new Slice(input.type, input, beginIndex, context.key, endIndex);
5441
+ } else return new Slice(input.type, input, beginIndex, context.key);
5363
5442
  }
5364
5443
  evaluate(ctx) {
5365
- const labels = this.labels;
5366
- const outputs = this.outputs;
5367
- if (labels.length === 1) return outputs[0].evaluate(ctx);
5368
- const value = this.input.evaluate(ctx);
5369
- if (value <= labels[0]) return outputs[0].evaluate(ctx);
5370
- const stopCount = labels.length;
5371
- if (value >= labels[stopCount - 1]) return outputs[stopCount - 1].evaluate(ctx);
5372
- const index = findStopLessThanOrEqualTo(labels, value, this.key);
5373
- const lower = labels[index];
5374
- const upper = labels[index + 1];
5375
- const t = Interpolate.interpolationFactor(this.interpolation, value, lower, upper);
5376
- const outputLower = outputs[index].evaluate(ctx);
5377
- const outputUpper = outputs[index + 1].evaluate(ctx);
5378
- switch (this.operator) {
5379
- case "interpolate": switch (this.type.kind) {
5380
- case "number": return interpolateNumber(outputLower, outputUpper, t);
5381
- case "color": return Color$1.interpolate(outputLower, outputUpper, t);
5382
- case "padding": return Padding.interpolate(outputLower, outputUpper, t);
5383
- case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t);
5384
- case "numberArray": return NumberArray.interpolate(outputLower, outputUpper, t);
5385
- case "variableAnchorOffsetCollection": return VariableAnchorOffsetCollection.interpolate(outputLower, outputUpper, t, this.key);
5386
- case "array": return interpolateArray(outputLower, outputUpper, t);
5387
- case "projectionDefinition": return ProjectionDefinition.interpolate(outputLower, outputUpper, t);
5388
- }
5389
- case "interpolate-hcl": switch (this.type.kind) {
5390
- case "color": return Color$1.interpolate(outputLower, outputUpper, t, "hcl");
5391
- case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t, "hcl");
5392
- }
5393
- case "interpolate-lab": switch (this.type.kind) {
5394
- case "color": return Color$1.interpolate(outputLower, outputUpper, t, "lab");
5395
- case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t, "lab");
5396
- }
5397
- }
5444
+ const input = this.input.evaluate(ctx);
5445
+ const beginIndex = this.beginIndex.evaluate(ctx);
5446
+ let endIndex;
5447
+ if (this.endIndex) endIndex = this.endIndex.evaluate(ctx);
5448
+ if (isValidNativeType(input, ["string"])) return [...input].slice(beginIndex, endIndex).join("");
5449
+ else if (isValidNativeType(input, ["array"])) return input.slice(beginIndex, endIndex);
5450
+ else throw new RuntimeError(`Expected first argument to be of type array or string, but found ${typeToString(typeOf(input))} instead.`, this.key);
5398
5451
  }
5399
5452
  eachChild(fn) {
5400
5453
  fn(this.input);
5401
- for (const expression of this.outputs) fn(expression);
5454
+ fn(this.beginIndex);
5455
+ if (this.endIndex) fn(this.endIndex);
5402
5456
  }
5403
5457
  outputDefined() {
5404
- return this.outputs.every((out) => out.outputDefined());
5458
+ return false;
5405
5459
  }
5406
5460
  };
5407
- /**
5408
- * Returns a ratio that can be used to interpolate between exponential function
5409
- * stops.
5410
- * 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,
5411
- * and `a` and `b` are constants affording sufficient degrees of freedom to fit
5412
- * the function to the given stops.
5413
- *
5414
- * Here's a bit of algebra that lets us compute `f(x)` directly from the stop
5415
- * values without explicitly solving for `a` and `b`:
5416
- *
5417
- * First stop value: `f(x0) = y0 = a * base^x0 + b`
5418
- * Second stop value: `f(x1) = y1 = a * base^x1 + b`
5419
- * => `y1 - y0 = a(base^x1 - base^x0)`
5420
- * => `a = (y1 - y0)/(base^x1 - base^x0)`
5421
- *
5422
- * Desired value: `f(x) = y = a * base^x + b`
5423
- * => `f(x) = y0 + a * (base^x - base^x0)`
5424
- *
5425
- * From the above, we can replace the `a` in `a * (base^x - base^x0)` and do a
5426
- * little algebra:
5427
- * ```
5428
- * a * (base^x - base^x0) = (y1 - y0)/(base^x1 - base^x0) * (base^x - base^x0)
5429
- * = (y1 - y0) * (base^x - base^x0) / (base^x1 - base^x0)
5430
- * ```
5431
- *
5432
- * If we let `(base^x - base^x0) / (base^x1 base^x0)`, then we have
5433
- * `f(x) = y0 + (y1 - y0) * ratio`. In other words, `ratio` may be treated as
5434
- * an interpolation factor between the two stops' output values.
5435
- *
5436
- * (Note: a slightly different form for `ratio`,
5437
- * `(base^(x-x0) - 1) / (base^(x1-x0) - 1) `, is equivalent, but requires fewer
5438
- * expensive `Math.pow()` operations.)
5439
- *
5440
- * @private
5441
- */
5442
- function exponentialInterpolation(input, base, lowerValue, upperValue) {
5443
- const difference = upperValue - lowerValue;
5444
- const progress = input - lowerValue;
5445
- if (difference === 0) return 0;
5446
- else if (base === 1) return progress / difference;
5447
- else return (Math.pow(base, progress) - 1) / (Math.pow(base, difference) - 1);
5448
- }
5449
- const interpolateFactory = {
5450
- color: Color$1.interpolate,
5451
- number: interpolateNumber,
5452
- padding: Padding.interpolate,
5453
- numberArray: NumberArray.interpolate,
5454
- colorArray: ColorArray.interpolate,
5455
- variableAnchorOffsetCollection: VariableAnchorOffsetCollection.interpolate,
5456
- array: interpolateArray
5457
- };
5458
- //#endregion
5461
+
5459
5462
  //#region src/expression/definitions/coalesce.ts
5460
5463
  var Coalesce = class Coalesce {
5461
5464
  constructor(type, args) {
@@ -5500,7 +5503,7 @@ var Coalesce = class Coalesce {
5500
5503
  return this.args.every((arg) => arg.outputDefined());
5501
5504
  }
5502
5505
  };
5503
- //#endregion
5506
+
5504
5507
  //#region src/expression/definitions/comparison.ts
5505
5508
  function isComparableType(op, type) {
5506
5509
  if (op === "==" || op === "!=") return type.kind === "boolean" || type.kind === "string" || type.kind === "number" || type.kind === "null" || type.kind === "value";
@@ -5623,7 +5626,7 @@ const LessThan = makeComparison("<", lt, ltCollate);
5623
5626
  const GreaterThan = makeComparison(">", gt, gtCollate);
5624
5627
  const LessThanOrEqual = makeComparison("<=", lteq, lteqCollate);
5625
5628
  const GreaterThanOrEqual = makeComparison(">=", gteq, gteqCollate);
5626
- //#endregion
5629
+
5627
5630
  //#region src/expression/definitions/collator.ts
5628
5631
  var CollatorExpression = class CollatorExpression {
5629
5632
  constructor(caseSensitive, diacriticSensitive, locale) {
@@ -5659,7 +5662,7 @@ var CollatorExpression = class CollatorExpression {
5659
5662
  return false;
5660
5663
  }
5661
5664
  };
5662
- //#endregion
5665
+
5663
5666
  //#region src/expression/definitions/number_format.ts
5664
5667
  var NumberFormat = class NumberFormat {
5665
5668
  constructor(number, locale, currency, unit, minFractionDigits, maxFractionDigits) {
@@ -5711,103 +5714,22 @@ var NumberFormat = class NumberFormat {
5711
5714
  currency: this.currency ? this.currency.evaluate(ctx) : void 0,
5712
5715
  unit: this.unit ? this.unit.evaluate(ctx) : void 0,
5713
5716
  minimumFractionDigits: this.minFractionDigits ? this.minFractionDigits.evaluate(ctx) : void 0,
5714
- maximumFractionDigits: this.maxFractionDigits ? this.maxFractionDigits.evaluate(ctx) : void 0
5715
- }).format(this.number.evaluate(ctx));
5716
- }
5717
- eachChild(fn) {
5718
- fn(this.number);
5719
- if (this.locale) fn(this.locale);
5720
- if (this.currency) fn(this.currency);
5721
- if (this.unit) fn(this.unit);
5722
- if (this.minFractionDigits) fn(this.minFractionDigits);
5723
- if (this.maxFractionDigits) fn(this.maxFractionDigits);
5724
- }
5725
- outputDefined() {
5726
- return false;
5727
- }
5728
- };
5729
- //#endregion
5730
- //#region src/expression/definitions/format.ts
5731
- var FormatExpression = class FormatExpression {
5732
- constructor(sections) {
5733
- this.type = FormattedType;
5734
- this.sections = sections;
5735
- }
5736
- static parse(args, context) {
5737
- if (args.length < 2) return context.error("Expected at least one argument.");
5738
- const firstArg = args[1];
5739
- if (!Array.isArray(firstArg) && typeof firstArg === "object") return context.error("First argument must be an image or text section.");
5740
- const sections = [];
5741
- let nextTokenMayBeObject = false;
5742
- for (let i = 1; i <= args.length - 1; ++i) {
5743
- const arg = args[i];
5744
- if (nextTokenMayBeObject && typeof arg === "object" && !Array.isArray(arg)) {
5745
- nextTokenMayBeObject = false;
5746
- let scale = null;
5747
- if (arg["font-scale"]) {
5748
- scale = context.parse(arg["font-scale"], 1, NumberType);
5749
- if (!scale) return null;
5750
- }
5751
- let font = null;
5752
- if (arg["text-font"]) {
5753
- font = context.parse(arg["text-font"], 1, array(StringType));
5754
- if (!font) return null;
5755
- }
5756
- let textColor = null;
5757
- if (arg["text-color"]) {
5758
- textColor = context.parse(arg["text-color"], 1, ColorType);
5759
- if (!textColor) return null;
5760
- }
5761
- let verticalAlign = null;
5762
- if (arg["vertical-align"]) {
5763
- 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.`);
5764
- verticalAlign = context.parse(arg["vertical-align"], 1, StringType);
5765
- if (!verticalAlign) return null;
5766
- }
5767
- const lastExpression = sections[sections.length - 1];
5768
- lastExpression.scale = scale;
5769
- lastExpression.font = font;
5770
- lastExpression.textColor = textColor;
5771
- lastExpression.verticalAlign = verticalAlign;
5772
- } else {
5773
- const content = context.parse(args[i], 1, ValueType);
5774
- if (!content) return null;
5775
- const kind = content.type.kind;
5776
- if (kind !== "string" && kind !== "value" && kind !== "null" && kind !== "resolvedImage") return context.error("Formatted text type must be 'string', 'value', 'image' or 'null'.");
5777
- nextTokenMayBeObject = true;
5778
- sections.push({
5779
- content,
5780
- scale: null,
5781
- font: null,
5782
- textColor: null,
5783
- verticalAlign: null
5784
- });
5785
- }
5786
- }
5787
- return new FormatExpression(sections);
5788
- }
5789
- evaluate(ctx) {
5790
- const evaluateSection = (section) => {
5791
- const evaluatedContent = section.content.evaluate(ctx);
5792
- if (typeOf(evaluatedContent) === ResolvedImageType) return new FormattedSection("", evaluatedContent, null, null, null, section.verticalAlign ? section.verticalAlign.evaluate(ctx) : null);
5793
- 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);
5794
- };
5795
- return new Formatted(this.sections.map(evaluateSection));
5717
+ maximumFractionDigits: this.maxFractionDigits ? this.maxFractionDigits.evaluate(ctx) : void 0
5718
+ }).format(this.number.evaluate(ctx));
5796
5719
  }
5797
5720
  eachChild(fn) {
5798
- for (const section of this.sections) {
5799
- fn(section.content);
5800
- if (section.scale) fn(section.scale);
5801
- if (section.font) fn(section.font);
5802
- if (section.textColor) fn(section.textColor);
5803
- if (section.verticalAlign) fn(section.verticalAlign);
5804
- }
5721
+ fn(this.number);
5722
+ if (this.locale) fn(this.locale);
5723
+ if (this.currency) fn(this.currency);
5724
+ if (this.unit) fn(this.unit);
5725
+ if (this.minFractionDigits) fn(this.minFractionDigits);
5726
+ if (this.maxFractionDigits) fn(this.maxFractionDigits);
5805
5727
  }
5806
5728
  outputDefined() {
5807
5729
  return false;
5808
5730
  }
5809
5731
  };
5810
- //#endregion
5732
+
5811
5733
  //#region src/expression/definitions/image.ts
5812
5734
  var ImageExpression = class ImageExpression {
5813
5735
  constructor(input) {
@@ -5833,7 +5755,7 @@ var ImageExpression = class ImageExpression {
5833
5755
  return false;
5834
5756
  }
5835
5757
  };
5836
- //#endregion
5758
+
5837
5759
  //#region src/expression/definitions/length.ts
5838
5760
  var Length = class Length {
5839
5761
  constructor(input, key) {
@@ -5861,7 +5783,7 @@ var Length = class Length {
5861
5783
  return false;
5862
5784
  }
5863
5785
  };
5864
- //#endregion
5786
+
5865
5787
  //#region src/util/geometry_util.ts
5866
5788
  const EXTENT = 8192;
5867
5789
  function getTileCoordinates(p, canonical) {
@@ -5957,7 +5879,7 @@ function twoSided(p1, p2, q1, q2) {
5957
5879
  if (det1 > 0 && det2 < 0 || det1 < 0 && det2 > 0) return true;
5958
5880
  return false;
5959
5881
  }
5960
- //#endregion
5882
+
5961
5883
  //#region src/expression/definitions/within.ts
5962
5884
  function getTilePolygon(coordinates, bbox, canonical) {
5963
5885
  const polygon = [];
@@ -6119,7 +6041,7 @@ var Within = class Within {
6119
6041
  return true;
6120
6042
  }
6121
6043
  };
6122
- //#endregion
6044
+
6123
6045
  //#region node_modules/tinyqueue/index.js
6124
6046
  var TinyQueue = class {
6125
6047
  constructor(data = [], compare = (a, b) => a < b ? -1 : a > b ? 1 : 0) {
@@ -6172,54 +6094,10 @@ var TinyQueue = class {
6172
6094
  data[pos] = item;
6173
6095
  }
6174
6096
  };
6175
- //#endregion
6176
- //#region src/util/classify_rings.ts
6177
- /**
6178
- * Classifies an array of rings into polygons with outer rings and holes
6179
- * @param rings - the rings to classify
6180
- * @param maxRings - the maximum number of rings to include in a polygon, use 0 to include all rings
6181
- * @returns an array of polygons with internal rings as holes
6182
- */
6183
- function classifyRings$1(rings, maxRings) {
6184
- if (rings.length <= 1) return [rings];
6185
- const polygons = [];
6186
- let polygon;
6187
- let ccw;
6188
- for (const ring of rings) {
6189
- const area = calculateSignedArea(ring);
6190
- if (area === 0) continue;
6191
- ring.area = Math.abs(area);
6192
- if (ccw === void 0) ccw = area < 0;
6193
- if (ccw === area < 0) {
6194
- if (polygon) polygons.push(polygon);
6195
- polygon = [ring];
6196
- } else polygon.push(ring);
6197
- }
6198
- if (polygon) polygons.push(polygon);
6199
- return polygons;
6200
- }
6201
- /**
6202
- * Returns the signed area for the polygon ring. Positive areas are exterior rings and
6203
- * have a clockwise winding. Negative areas are interior rings and have a counter clockwise
6204
- * ordering.
6205
- *
6206
- * @param ring - Exterior or interior ring
6207
- * @returns Signed area
6208
- */
6209
- function calculateSignedArea(ring) {
6210
- let sum = 0;
6211
- for (let i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) {
6212
- p1 = ring[i];
6213
- p2 = ring[j];
6214
- sum += (p2.x - p1.x) * (p1.y + p2.y);
6215
- }
6216
- return sum;
6217
- }
6218
- //#endregion
6097
+
6219
6098
  //#region src/util/cheap_ruler.ts
6220
6099
  const RE = 6378.137;
6221
- const FE = 1 / 298.257223563;
6222
- const E2 = FE * (2 - FE);
6100
+ const E2 = .0066943799901413165;
6223
6101
  const RAD = Math.PI / 180;
6224
6102
  var CheapRuler = class {
6225
6103
  constructor(lat) {
@@ -6228,7 +6106,7 @@ var CheapRuler = class {
6228
6106
  const w2 = 1 / (1 - E2 * (1 - coslat * coslat));
6229
6107
  const w = Math.sqrt(w2);
6230
6108
  this.kx = m * w * coslat;
6231
- this.ky = m * w * w2 * (1 - E2);
6109
+ this.ky = m * w * w2 * .9933056200098587;
6232
6110
  }
6233
6111
  /**
6234
6112
  * Given two points of the form [longitude, latitude], returns the distance.
@@ -6299,7 +6177,7 @@ var CheapRuler = class {
6299
6177
  return deg;
6300
6178
  }
6301
6179
  };
6302
- //#endregion
6180
+
6303
6181
  //#region src/expression/definitions/distance.ts
6304
6182
  const MinPointsSize = 100;
6305
6183
  const MinLinePointsSize = 50;
@@ -6567,9 +6445,7 @@ function pointToGeometryDistance(ctx, geometries) {
6567
6445
  case "LineString":
6568
6446
  dist = Math.min(dist, pointSetToPointSetDistance(pointPosition, false, geometry.coordinates, true, ruler, dist));
6569
6447
  break;
6570
- case "Polygon":
6571
- dist = Math.min(dist, pointsToPolygonDistance(pointPosition, false, geometry.coordinates, ruler, dist));
6572
- break;
6448
+ case "Polygon": dist = Math.min(dist, pointsToPolygonDistance(pointPosition, false, geometry.coordinates, ruler, dist));
6573
6449
  }
6574
6450
  if (dist === 0) return dist;
6575
6451
  }
@@ -6589,9 +6465,7 @@ function lineStringToGeometryDistance(ctx, geometries) {
6589
6465
  case "LineString":
6590
6466
  dist = Math.min(dist, pointSetToPointSetDistance(linePositions, true, geometry.coordinates, true, ruler, dist));
6591
6467
  break;
6592
- case "Polygon":
6593
- dist = Math.min(dist, pointsToPolygonDistance(linePositions, true, geometry.coordinates, ruler, dist));
6594
- break;
6468
+ case "Polygon": dist = Math.min(dist, pointsToPolygonDistance(linePositions, true, geometry.coordinates, ruler, dist));
6595
6469
  }
6596
6470
  if (dist === 0) return dist;
6597
6471
  }
@@ -6615,9 +6489,7 @@ function polygonToGeometryDistance(ctx, geometries) {
6615
6489
  case "LineString":
6616
6490
  dist = Math.min(dist, pointsToPolygonDistance(geometry.coordinates, true, polygon, ruler, dist));
6617
6491
  break;
6618
- case "Polygon":
6619
- dist = Math.min(dist, polygonToPolygonDistance(polygon, geometry.coordinates, ruler, dist));
6620
- break;
6492
+ case "Polygon": dist = Math.min(dist, polygonToPolygonDistance(polygon, geometry.coordinates, ruler, dist));
6621
6493
  }
6622
6494
  if (dist === 0) return dist;
6623
6495
  }
@@ -6673,7 +6545,48 @@ var Distance = class Distance {
6673
6545
  return true;
6674
6546
  }
6675
6547
  };
6676
- //#endregion
6548
+
6549
+ //#region src/expression/definitions/semiliteral.ts
6550
+ var Semiliteral = class Semiliteral {
6551
+ constructor(arr) {
6552
+ let elementType = null;
6553
+ for (const expr of arr) if (!elementType) elementType = expr.type;
6554
+ else if (elementType === expr.type) continue;
6555
+ else {
6556
+ elementType = ValueType;
6557
+ break;
6558
+ }
6559
+ this.type = array(elementType ?? ValueType, arr.length);
6560
+ this.arr = arr;
6561
+ }
6562
+ static parse(args, context) {
6563
+ if (args.length !== 2) return context.error(`'semiliteral' expression requires exactly one argument, but found ${args.length - 1} instead.`);
6564
+ if (!isValue(args[1])) return context.error(`invalid value of type "${typeof args[1]}"`);
6565
+ const value = args[1];
6566
+ const type = typeOf(value);
6567
+ if (type.kind === "array") {
6568
+ const arr = value;
6569
+ const arrayContext = context.concat(1);
6570
+ const parsed = [];
6571
+ for (let i = 0; i < arr.length; i++) {
6572
+ const item = arrayContext.parse(arr[i], i, ValueType);
6573
+ if (!item) return null;
6574
+ parsed.push(item);
6575
+ }
6576
+ return new Semiliteral(parsed);
6577
+ } else return new Literal(type, value);
6578
+ }
6579
+ evaluate(ctx) {
6580
+ return this.arr.map((arg) => arg.evaluate(ctx));
6581
+ }
6582
+ eachChild(fn) {
6583
+ this.arr.forEach(fn);
6584
+ }
6585
+ outputDefined() {
6586
+ return this.arr.every((arg) => arg.outputDefined());
6587
+ }
6588
+ };
6589
+
6677
6590
  //#region src/expression/definitions/global_state.ts
6678
6591
  var GlobalState = class GlobalState {
6679
6592
  constructor(key) {
@@ -6697,7 +6610,7 @@ var GlobalState = class GlobalState {
6697
6610
  return false;
6698
6611
  }
6699
6612
  };
6700
- //#endregion
6613
+
6701
6614
  //#region src/expression/definitions/index.ts
6702
6615
  const expressions = {
6703
6616
  "==": Equals,
@@ -6726,6 +6639,7 @@ const expressions = {
6726
6639
  number: Assertion,
6727
6640
  "number-format": NumberFormat,
6728
6641
  object: Assertion,
6642
+ semiliteral: Semiliteral,
6729
6643
  slice: Slice,
6730
6644
  step: Step,
6731
6645
  string: Assertion,
@@ -6738,7 +6652,163 @@ const expressions = {
6738
6652
  distance: Distance,
6739
6653
  "global-state": GlobalState
6740
6654
  };
6741
- //#endregion
6655
+
6656
+ //#region src/expression/parsing_error.ts
6657
+ var ExpressionParsingError = class extends Error {
6658
+ constructor(key, message) {
6659
+ super(message);
6660
+ this.message = message;
6661
+ this.key = key;
6662
+ }
6663
+ };
6664
+
6665
+ //#region src/expression/scope.ts
6666
+ /**
6667
+ * Tracks `let` bindings during expression parsing.
6668
+ * @private
6669
+ */
6670
+ var Scope = class Scope {
6671
+ constructor(parent, bindings = []) {
6672
+ this.parent = parent;
6673
+ this.bindings = {};
6674
+ for (const [name, expression] of bindings) this.bindings[name] = expression;
6675
+ }
6676
+ concat(bindings) {
6677
+ return new Scope(this, bindings);
6678
+ }
6679
+ get(name) {
6680
+ if (this.bindings[name]) return this.bindings[name];
6681
+ if (this.parent) return this.parent.get(name);
6682
+ throw new Error(`${name} not found in scope.`);
6683
+ }
6684
+ has(name) {
6685
+ if (this.bindings[name]) return true;
6686
+ return this.parent ? this.parent.has(name) : false;
6687
+ }
6688
+ };
6689
+
6690
+ //#region src/expression/parsing_context.ts
6691
+ /**
6692
+ * State associated parsing at a given point in an expression tree.
6693
+ * @private
6694
+ */
6695
+ var ParsingContext = class ParsingContext {
6696
+ constructor(registry, isConstantFunc, path = [], expectedType, scope = new Scope(), errors = []) {
6697
+ this.registry = registry;
6698
+ this.path = path;
6699
+ this.key = path.map((part) => `[${part}]`).join("");
6700
+ this.scope = scope;
6701
+ this.errors = errors;
6702
+ this.expectedType = expectedType;
6703
+ this._isConstant = isConstantFunc;
6704
+ }
6705
+ /**
6706
+ * @param expr the JSON expression to parse
6707
+ * @param index the optional argument index if this expression is an argument of a parent expression that's being parsed
6708
+ * @param options
6709
+ * @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.
6710
+ * @private
6711
+ */
6712
+ parse(expr, index, expectedType, bindings, options = {}) {
6713
+ if (index != null) return this.concat(index, expectedType, bindings)._parse(expr, options);
6714
+ return this._parse(expr, options);
6715
+ }
6716
+ _parse(expr, options) {
6717
+ if (expr === null || typeof expr === "string" || typeof expr === "boolean" || typeof expr === "number") expr = ["literal", expr];
6718
+ const key = this.key;
6719
+ function annotate(parsed, type, typeAnnotation) {
6720
+ if (typeAnnotation === "assert") return new Assertion(type, [parsed], key);
6721
+ else if (typeAnnotation === "coerce") return new Coercion(type, [parsed], key);
6722
+ else return parsed;
6723
+ }
6724
+ if (Array.isArray(expr)) {
6725
+ if (expr.length === 0) return this.error("Expected an array with at least one element. If you wanted a literal array, use [\"literal\", []].");
6726
+ const op = expr[0];
6727
+ if (typeof op !== "string") {
6728
+ this.error(`Expression name must be a string, but found ${typeof op} instead. If you wanted a literal array, use ["literal", [...]].`, 0);
6729
+ return null;
6730
+ }
6731
+ const Expr = this.registry[op];
6732
+ if (Expr) {
6733
+ let parsed = Expr.parse(expr, this);
6734
+ if (!parsed) return null;
6735
+ if (this.expectedType) {
6736
+ const expected = this.expectedType;
6737
+ const actual = parsed.type;
6738
+ 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");
6739
+ else if ("projectionDefinition" === expected.kind && [
6740
+ "string",
6741
+ "array",
6742
+ "value"
6743
+ ].includes(actual.kind) || [
6744
+ "color",
6745
+ "formatted",
6746
+ "resolvedImage"
6747
+ ].includes(expected.kind) && ["value", "string"].includes(actual.kind) || ["padding", "numberArray"].includes(expected.kind) && [
6748
+ "value",
6749
+ "number",
6750
+ "array"
6751
+ ].includes(actual.kind) || "colorArray" === expected.kind && [
6752
+ "value",
6753
+ "string",
6754
+ "array"
6755
+ ].includes(actual.kind) || "variableAnchorOffsetCollection" === expected.kind && ["value", "array"].includes(actual.kind)) parsed = annotate(parsed, expected, options.typeAnnotation || "coerce");
6756
+ else if (this.checkSubtype(expected, actual)) return null;
6757
+ }
6758
+ if (!(parsed instanceof Literal) && parsed.type.kind !== "resolvedImage" && this._isConstant(parsed)) {
6759
+ const ec = new EvaluationContext();
6760
+ try {
6761
+ parsed = new Literal(parsed.type, parsed.evaluate(ec));
6762
+ } catch (e) {
6763
+ this.error(e.message);
6764
+ return null;
6765
+ }
6766
+ }
6767
+ return parsed;
6768
+ }
6769
+ return this.error(`Unknown expression "${op}". If you wanted a literal array, use ["literal", [...]].`, 0);
6770
+ } else if (typeof expr === "undefined") return this.error("'undefined' value invalid. Use null instead.");
6771
+ else if (typeof expr === "object") return this.error("Bare objects invalid. Use [\"literal\", {...}] instead.");
6772
+ else return this.error(`Expected an array, but found ${typeof expr} instead.`);
6773
+ }
6774
+ /**
6775
+ * Returns a copy of this context suitable for parsing the subexpression at
6776
+ * index `index`, optionally appending to 'let' binding map.
6777
+ *
6778
+ * Note that `errors` property, intended for collecting errors while
6779
+ * parsing, is copied by reference rather than cloned.
6780
+ * @private
6781
+ */
6782
+ concat(index, expectedType, bindings) {
6783
+ const path = typeof index === "number" ? this.path.concat(index) : this.path;
6784
+ const scope = bindings ? this.scope.concat(bindings) : this.scope;
6785
+ return new ParsingContext(this.registry, this._isConstant, path, expectedType || null, scope, this.errors);
6786
+ }
6787
+ /**
6788
+ * Push a parsing (or type checking) error into the `this.errors`
6789
+ * @param error The message
6790
+ * @param keys Optionally specify the source of the error at a child
6791
+ * of the current expression at `this.key`.
6792
+ * @private
6793
+ */
6794
+ error(error, ...keys) {
6795
+ const key = `${this.key}${keys.map((k) => `[${k}]`).join("")}`;
6796
+ this.errors.push(new ExpressionParsingError(key, error));
6797
+ }
6798
+ /**
6799
+ * Returns null if `t` is a subtype of `expected`; otherwise returns an
6800
+ * error message and also pushes it to `this.errors`.
6801
+ * @param expected The expected type
6802
+ * @param t The actual type
6803
+ * @returns null if `t` is a subtype of `expected`; otherwise returns an error message
6804
+ */
6805
+ checkSubtype(expected, t) {
6806
+ const error = checkSubtype(expected, t);
6807
+ if (error) this.error(error);
6808
+ return error;
6809
+ }
6810
+ };
6811
+
6742
6812
  //#region src/expression/compound_expression.ts
6743
6813
  var CompoundExpression = class CompoundExpression {
6744
6814
  constructor(name, type, evaluate, args, key) {
@@ -7306,28 +7376,14 @@ function isStateConstant(e) {
7306
7376
  return result;
7307
7377
  }
7308
7378
  function isGlobalPropertyConstant(e, properties) {
7309
- if (e instanceof CompoundExpression && properties.indexOf(e.name) >= 0) return false;
7310
- let result = true;
7311
- e.eachChild((arg) => {
7312
- if (result && !isGlobalPropertyConstant(arg, properties)) result = false;
7313
- });
7314
- return result;
7315
- }
7316
- //#endregion
7317
- //#region src/util/result.ts
7318
- function success(value) {
7319
- return {
7320
- result: "success",
7321
- value
7322
- };
7323
- }
7324
- function error(value) {
7325
- return {
7326
- result: "error",
7327
- value
7328
- };
7379
+ if (e instanceof CompoundExpression && properties.indexOf(e.name) >= 0) return false;
7380
+ let result = true;
7381
+ e.eachChild((arg) => {
7382
+ if (result && !isGlobalPropertyConstant(arg, properties)) result = false;
7383
+ });
7384
+ return result;
7329
7385
  }
7330
- //#endregion
7386
+
7331
7387
  //#region src/util/properties.ts
7332
7388
  function supportsPropertyExpression(spec) {
7333
7389
  return spec["property-type"] === "data-driven" || spec["property-type"] === "cross-faded-data-driven";
@@ -7338,13 +7394,13 @@ function supportsZoomExpression(spec) {
7338
7394
  function supportsInterpolation(spec) {
7339
7395
  return !!spec.expression && spec.expression.interpolated;
7340
7396
  }
7341
- //#endregion
7397
+
7342
7398
  //#region src/util/extend.ts
7343
7399
  function extendBy(output, ...inputs) {
7344
7400
  for (const input of inputs) for (const k in input) output[k] = input[k];
7345
7401
  return output;
7346
7402
  }
7347
- //#endregion
7403
+
7348
7404
  //#region src/util/get_type.ts
7349
7405
  function getType(val) {
7350
7406
  if (val instanceof Number) return "number";
@@ -7354,7 +7410,7 @@ function getType(val) {
7354
7410
  else if (val === null) return "null";
7355
7411
  else return typeof val;
7356
7412
  }
7357
- //#endregion
7413
+
7358
7414
  //#region src/function/index.ts
7359
7415
  function isFunction(value) {
7360
7416
  return typeof value === "object" && value !== null && !Array.isArray(value) && typeOf(value) === ObjectType;
@@ -7452,21 +7508,21 @@ function createFunction(parameters, propertySpec) {
7452
7508
  kind: "source",
7453
7509
  evaluate(_, feature) {
7454
7510
  const value = feature && feature.properties ? feature.properties[parameters.property] : void 0;
7455
- if (value === void 0) return coalesce$1(parameters.default, propertySpec.default);
7511
+ if (value === void 0) return coalesce(parameters.default, propertySpec.default);
7456
7512
  return innerFun(parameters, propertySpec, value, hashedStops, categoricalKeyType);
7457
7513
  }
7458
7514
  };
7459
7515
  }
7460
- function coalesce$1(a, b, c) {
7516
+ function coalesce(a, b, c) {
7461
7517
  if (a !== void 0) return a;
7462
7518
  if (b !== void 0) return b;
7463
7519
  if (c !== void 0) return c;
7464
7520
  }
7465
7521
  function evaluateCategoricalFunction(parameters, propertySpec, input, hashedStops, keyType) {
7466
- return coalesce$1(typeof input === keyType ? hashedStops[input] : void 0, parameters.default, propertySpec.default);
7522
+ return coalesce(typeof input === keyType ? hashedStops[input] : void 0, parameters.default, propertySpec.default);
7467
7523
  }
7468
7524
  function evaluateIntervalFunction(parameters, propertySpec, input) {
7469
- if (getType(input) !== "number") return coalesce$1(parameters.default, propertySpec.default);
7525
+ if (getType(input) !== "number") return coalesce(parameters.default, propertySpec.default);
7470
7526
  const n = parameters.stops.length;
7471
7527
  if (n === 1) return parameters.stops[0][1];
7472
7528
  if (input <= parameters.stops[0][0]) return parameters.stops[0][1];
@@ -7476,13 +7532,13 @@ function evaluateIntervalFunction(parameters, propertySpec, input) {
7476
7532
  }
7477
7533
  function evaluateExponentialFunction(parameters, propertySpec, input) {
7478
7534
  const base = parameters.base !== void 0 ? parameters.base : 1;
7479
- if (getType(input) !== "number") return coalesce$1(parameters.default, propertySpec.default);
7535
+ if (getType(input) !== "number") return coalesce(parameters.default, propertySpec.default);
7480
7536
  const n = parameters.stops.length;
7481
7537
  if (n === 1) return parameters.stops[0][1];
7482
7538
  if (input <= parameters.stops[0][0]) return parameters.stops[0][1];
7483
7539
  if (input >= parameters.stops[n - 1][0]) return parameters.stops[n - 1][1];
7484
7540
  const index = findStopLessThanOrEqualTo(parameters.stops.map((stop) => stop[0]), input, "");
7485
- const t = interpolationFactor(input, base, parameters.stops[index][0], parameters.stops[index + 1][0]);
7541
+ const t = interpolationFactor$1(input, base, parameters.stops[index][0], parameters.stops[index + 1][0]);
7486
7542
  const outputLower = parameters.stops[index][1];
7487
7543
  const outputUpper = parameters.stops[index + 1][1];
7488
7544
  const interp = interpolateFactory[propertySpec.type] || identityFunction;
@@ -7516,7 +7572,7 @@ function evaluateIdentityFunction(parameters, propertySpec, input) {
7516
7572
  break;
7517
7573
  default: if (getType(input) !== propertySpec.type && (propertySpec.type !== "enum" || !propertySpec.values[input])) input = void 0;
7518
7574
  }
7519
- return coalesce$1(input, parameters.default, propertySpec.default);
7575
+ return coalesce(input, parameters.default, propertySpec.default);
7520
7576
  }
7521
7577
  /**
7522
7578
  * Returns a ratio that can be used to interpolate between exponential function
@@ -7556,14 +7612,28 @@ function evaluateIdentityFunction(parameters, propertySpec, input) {
7556
7612
  *
7557
7613
  * @private
7558
7614
  */
7559
- function interpolationFactor(input, base, lowerValue, upperValue) {
7615
+ function interpolationFactor$1(input, base, lowerValue, upperValue) {
7560
7616
  const difference = upperValue - lowerValue;
7561
7617
  const progress = input - lowerValue;
7562
7618
  if (difference === 0) return 0;
7563
7619
  else if (base === 1) return progress / difference;
7564
7620
  else return (Math.pow(base, progress) - 1) / (Math.pow(base, difference) - 1);
7565
7621
  }
7566
- //#endregion
7622
+
7623
+ //#region src/util/result.ts
7624
+ function success(value) {
7625
+ return {
7626
+ result: "success",
7627
+ value
7628
+ };
7629
+ }
7630
+ function error(value) {
7631
+ return {
7632
+ result: "error",
7633
+ value
7634
+ };
7635
+ }
7636
+
7567
7637
  //#region src/expression/index.ts
7568
7638
  var StyleExpression = class {
7569
7639
  constructor(expression, rootKey, propertySpec, globalState) {
@@ -7845,7 +7915,7 @@ function addGlobalState(globals, globalState) {
7845
7915
  globalState
7846
7916
  };
7847
7917
  }
7848
- //#endregion
7918
+
7849
7919
  //#region src/feature_filter/index.ts
7850
7920
  function classifyChildren(children) {
7851
7921
  let sawLegacy = false;
@@ -7951,12 +8021,10 @@ function findMixedLegacyFilter(filter, path = []) {
7951
8021
  if (diagnostic) return diagnostic;
7952
8022
  break;
7953
8023
  }
7954
- case "case":
7955
- for (let i = 1; i < filter.length - 1; i += 2) {
7956
- const diagnostic = checkChild(i, path, filter);
7957
- if (diagnostic) return diagnostic;
7958
- }
7959
- break;
8024
+ case "case": for (let i = 1; i < filter.length - 1; i += 2) {
8025
+ const diagnostic = checkChild(i, path, filter);
8026
+ if (diagnostic) return diagnostic;
8027
+ }
7960
8028
  }
7961
8029
  return null;
7962
8030
  }
@@ -7994,7 +8062,7 @@ function featureFilter(filter, rootKey, globalState) {
7994
8062
  needGeometry: false,
7995
8063
  getGlobalStateRefs: () => /* @__PURE__ */ new Set()
7996
8064
  };
7997
- if (!isExpressionFilter(filter)) filter = convertFilter$1(filter);
8065
+ if (!isExpressionFilter(filter)) filter = convertFilter(filter);
7998
8066
  else warnAboutMixedLegacyFilter(filter, rootKey);
7999
8067
  const compiled = createExpression(filter, rootKey, filterSpec, globalState);
8000
8068
  if (compiled.result === "error") throw new Error(compiled.value.map((err) => `${err.key}: ${err.message}`).join(", "));
@@ -8013,13 +8081,13 @@ function geometryNeeded(filter) {
8013
8081
  for (let index = 1; index < filter.length; index++) if (geometryNeeded(filter[index])) return true;
8014
8082
  return false;
8015
8083
  }
8016
- function convertFilter$1(filter) {
8084
+ function convertFilter(filter) {
8017
8085
  if (!filter) return true;
8018
8086
  const op = filter[0];
8019
8087
  if (filter.length <= 1) return op !== "any";
8020
- 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;
8088
+ 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;
8021
8089
  }
8022
- function convertComparisonOp$1(property, value, op) {
8090
+ function convertComparisonOp(property, value, op) {
8023
8091
  switch (property) {
8024
8092
  case "$type": return [`filter-type-${op}`, value];
8025
8093
  case "$id": return [`filter-id-${op}`, value];
@@ -8031,9 +8099,9 @@ function convertComparisonOp$1(property, value, op) {
8031
8099
  }
8032
8100
  }
8033
8101
  function convertDisjunctionOp(filters) {
8034
- return ["any"].concat(filters.map(convertFilter$1));
8102
+ return ["any"].concat(filters.map(convertFilter));
8035
8103
  }
8036
- function convertInOp$1(property, values) {
8104
+ function convertInOp(property, values) {
8037
8105
  if (values.length === 0) return false;
8038
8106
  switch (property) {
8039
8107
  case "$type": return ["filter-type-in", ["literal", values]];
@@ -8050,7 +8118,7 @@ function convertInOp$1(property, values) {
8050
8118
  ];
8051
8119
  }
8052
8120
  }
8053
- function convertHasOp$1(property) {
8121
+ function convertHasOp(property) {
8054
8122
  switch (property) {
8055
8123
  case "$type": return true;
8056
8124
  case "$id": return ["filter-has-id"];
@@ -8366,21 +8434,29 @@ class SVGRenderer {
8366
8434
  width;
8367
8435
  height;
8368
8436
  #svg;
8369
- #backgroundColor;
8370
8437
  #spriteSheetDefs = new Map();
8371
8438
  #spriteSymbolDefs = new Map();
8372
8439
  #sdfFilterDefs = new Map();
8373
8440
  #blurFilterDefs = new Map();
8441
+ #rasterDefs = [];
8442
+ #rasterClipCount = 0;
8443
+ #clipCircle;
8374
8444
  constructor(opt) {
8375
8445
  this.width = opt.width;
8376
8446
  this.height = opt.height;
8377
8447
  this.#svg = [];
8378
- this.#backgroundColor = Color.transparent;
8448
+ }
8449
+ setClipCircle(circle) {
8450
+ this.#clipCircle = circle;
8379
8451
  }
8380
8452
  drawBackgroundFill(style) {
8453
+ // Every background layer paints over what is below it, like any other layer. (A
8454
+ // transparent one, e.g. an empty "slot" layer, must not erase an earlier background.)
8381
8455
  const color = new Color(style.color);
8382
8456
  color.alpha *= style.opacity;
8383
- this.#backgroundColor = color;
8457
+ if (color.alpha <= 0)
8458
+ return;
8459
+ this.#svg.push(`<rect x="-1" y="-1" width="${(this.width + 2).toFixed(0)}" height="${(this.height + 2).toFixed(0)}" ${fillAttr(color)} />`);
8384
8460
  }
8385
8461
  drawPolygons(id, features) {
8386
8462
  if (features.length === 0)
@@ -8388,9 +8464,11 @@ class SVGRenderer {
8388
8464
  // Merge only *consecutive* features with identical attributes, so the paint
8389
8465
  // order of overlapping features matches MapLibre's source order. A global
8390
8466
  // merge would reorder interleaved features (e.g. data-driven fill colors).
8391
- // Fills and their antialias outlines are collected in one pass but emitted
8392
- // as two: MapLibre draws every fill first, then every outline, so a lower
8393
- // feature's outline composites on top of a later overlapping fill.
8467
+ // Translucent features are never merged: MapLibre blends each one separately,
8468
+ // so where they overlap, their opacity adds up — within one merged <path> it
8469
+ // would not. Fills and their antialias outlines are collected in one pass but
8470
+ // emitted as two: MapLibre draws every fill first, then every outline, so a
8471
+ // lower feature's outline composites on top of a later overlapping fill.
8394
8472
  const fillGroups = [];
8395
8473
  const outlineGroups = [];
8396
8474
  let currentFillKey;
@@ -8405,37 +8483,48 @@ class SVGRenderer {
8405
8483
  // Round each ring once; shared by the fill and the outline pass.
8406
8484
  const rings = feature.geometry.map((ring) => ring.map((p) => roundXY(p.x, p.y)));
8407
8485
  const color = new Color(style.color);
8486
+ const translucent = style.opacity < 1 || color.alpha < 255;
8408
8487
  if (color.alpha > 0) {
8409
8488
  const key = color.hex + translate + opacityAttr;
8410
- if (key !== currentFillKey) {
8489
+ if (translucent || key !== currentFillKey) {
8411
8490
  fillGroups.push({ segments: [], attrs: `${fillAttr(color)}${translate}${opacityAttr}` });
8412
- currentFillKey = key;
8491
+ currentFillKey = translucent ? undefined : key;
8413
8492
  }
8414
8493
  const group = fillGroups[fillGroups.length - 1];
8415
8494
  for (const ring of rings)
8416
8495
  group.segments.push(ring);
8417
8496
  }
8418
- // fill-antialias outline in an explicit fill-outline-color. MapLibre also
8419
- // draws a *default* outline in the fill color, but that outline IS its fill-
8420
- // edge antialiasing (draw_fill.ts clips it to just outside the shape) — and
8421
- // an SVG <path fill> is already rasterizer-antialiased, so redrawing it is
8422
- // redundant: measured identical pixel diff for +46–88% SVG size. We only
8423
- // draw the outline when a distinct color is set — the visible-border case
8424
- // (choropleths), which the browser's fill AA does not cover.
8425
- if (style.antialias && style.outlineColor !== undefined) {
8426
- const outlineColor = new Color(style.outlineColor);
8497
+ // fill-antialias outline, in fill-outline-color or else the fill color. For an
8498
+ // opaque fill, MapLibre's default outline IS its fill-edge antialiasing
8499
+ // (draw_fill.ts keeps it outside of the shape), and an SVG <path fill> is already
8500
+ // rasterizer-antialiased, so redrawing it is redundant: measured identical pixel
8501
+ // diff for +46–88% SVG size. It is drawn when it shows: in a distinct
8502
+ // fill-outline-color (choropleths), or for a translucent fill, where MapLibre
8503
+ // draws the outline over the fill's edge (e.g. buildings fading in).
8504
+ if (style.antialias && (style.outlineColor !== undefined || translucent)) {
8505
+ const outlineColor = style.outlineColor !== undefined ? new Color(style.outlineColor) : color;
8427
8506
  if (outlineColor.alpha > 0) {
8507
+ const outlineTranslucent = translucent || outlineColor.alpha < 255;
8428
8508
  const key = outlineColor.hex + translate + opacityAttr;
8429
- if (key !== currentOutlineKey) {
8509
+ if (outlineTranslucent || key !== currentOutlineKey) {
8430
8510
  outlineGroups.push({
8431
- segments: [],
8511
+ closed: [],
8512
+ open: [],
8432
8513
  attrs: `fill="none" ${strokeAttr(outlineColor, formatScaled(FILL_OUTLINE_WIDTH_PX))}${translate}${opacityAttr}`,
8433
8514
  });
8434
- currentOutlineKey = key;
8515
+ currentOutlineKey = outlineTranslucent ? undefined : key;
8435
8516
  }
8436
8517
  const group = outlineGroups[outlineGroups.length - 1];
8437
- for (const ring of rings)
8438
- group.segments.push(ring);
8518
+ // A polygon clipped to its tile has an outline without the clipped edges.
8519
+ if (feature.outline) {
8520
+ for (const line of feature.outline) {
8521
+ group.open.push(line.map((p) => roundXY(p.x, p.y)));
8522
+ }
8523
+ }
8524
+ else {
8525
+ for (const ring of rings)
8526
+ group.closed.push(ring);
8527
+ }
8439
8528
  }
8440
8529
  }
8441
8530
  });
@@ -8443,8 +8532,9 @@ class SVGRenderer {
8443
8532
  for (const { segments, attrs } of fillGroups) {
8444
8533
  this.#svg.push(`<path d="${segmentsToPath(segments, true)}" ${attrs} />`);
8445
8534
  }
8446
- for (const { segments, attrs } of outlineGroups) {
8447
- this.#svg.push(`<path d="${segmentsToPath(segments, true)}" ${attrs} />`);
8535
+ for (const { closed, open, attrs } of outlineGroups) {
8536
+ const d = segmentsToPath(closed, true) + segmentsToPath(open, false);
8537
+ this.#svg.push(`<path d="${d}" ${attrs} />`);
8448
8538
  }
8449
8539
  this.#svg.push('</g>');
8450
8540
  }
@@ -8452,7 +8542,12 @@ class SVGRenderer {
8452
8542
  if (features.length === 0)
8453
8543
  return;
8454
8544
  // Merge only *consecutive* same-attribute features (see drawPolygons) so the
8455
- // paint order of overlapping lines matches MapLibre's source order.
8545
+ // paint order of overlapping lines matches MapLibre's source order. Translucent
8546
+ // lines are never merged: MapLibre blends every line separately, so where lines
8547
+ // overlap (e.g. parallel rail tracks fading in), their opacity adds up — even
8548
+ // between the parts of one feature (tiles merge equal ways into one multi-line
8549
+ // feature). One <path> covers its overlaps only once, so a translucent line gets
8550
+ // a <path> per part.
8456
8551
  const groups = [];
8457
8552
  let currentKey;
8458
8553
  features.forEach(([feature, style]) => {
@@ -8487,7 +8582,8 @@ class SVGRenderer {
8487
8582
  translate,
8488
8583
  filterAttr,
8489
8584
  ].join('\0');
8490
- if (key !== currentKey) {
8585
+ const translucent = effectiveOpacity < 1 || color.alpha < 255;
8586
+ if (translucent || key !== currentKey) {
8491
8587
  const attrs = [
8492
8588
  'fill="none"',
8493
8589
  strokeAttr(color, roundedWidth),
@@ -8500,8 +8596,9 @@ class SVGRenderer {
8500
8596
  groups.push({
8501
8597
  segments: [],
8502
8598
  attrs: attrs.join(' ') + translate + opacityAttr + filterAttr,
8599
+ separate: translucent,
8503
8600
  });
8504
- currentKey = key;
8601
+ currentKey = translucent ? undefined : key;
8505
8602
  }
8506
8603
  const group = groups[groups.length - 1];
8507
8604
  feature.geometry.forEach((line) => {
@@ -8510,7 +8607,13 @@ class SVGRenderer {
8510
8607
  });
8511
8608
  });
8512
8609
  this.#svg.push(`<g id="${escapeXml(id)}">`);
8513
- for (const { segments, attrs } of groups) {
8610
+ for (const { segments, attrs, separate } of groups) {
8611
+ if (separate) {
8612
+ for (const segment of segments) {
8613
+ this.#svg.push(`<path d="${segmentsToPath([segment])}" ${attrs} />`);
8614
+ }
8615
+ continue;
8616
+ }
8514
8617
  const chains = chainSegments(segments);
8515
8618
  const d = segmentsToPath(chains);
8516
8619
  this.#svg.push(`<path d="${d}" ${attrs} />`);
@@ -8767,7 +8870,10 @@ class SVGRenderer {
8767
8870
  gAttrs += ` filter="${filters.join(' ')}"`;
8768
8871
  this.#svg.push(`<g ${gAttrs}>`);
8769
8872
  const pixelated = style.resampling === 'nearest';
8873
+ this.#drawRasterMeshes(tiles, pixelated);
8770
8874
  for (const tile of tiles) {
8875
+ if (tile.triangles)
8876
+ continue;
8771
8877
  const overlap = Math.min(tile.width, tile.height) / 10000; // slight overlap to prevent sub-pixel gaps between tiles
8772
8878
  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}"`;
8773
8879
  if (pixelated)
@@ -8776,11 +8882,65 @@ class SVGRenderer {
8776
8882
  }
8777
8883
  this.#svg.push('</g>');
8778
8884
  }
8885
+ /**
8886
+ * Draws raster tiles given as meshes of triangles (globe projection). Every tile image is
8887
+ * defined once (as a square of RASTER_TILE_UNITS); every triangle shows it through the affine
8888
+ * transform that maps its three source corners exactly onto its three screen corners, clipped
8889
+ * to the triangle on screen.
8890
+ *
8891
+ * Seams: within a tile, each clip triangle is grown by RASTER_TRIANGLE_OVERLAP_PX; the same
8892
+ * transform continues there, so the overlap shows (almost) the same pixels as the neighbour.
8893
+ * At tile borders that is not enough, as both images end on the same line and their anti-
8894
+ * aliased edges let a hairline of the background shine through. So first, as an underlay,
8895
+ * the triangles along the tile borders are drawn with their image reaching beyond the border
8896
+ * (see {@link bleedAtTileBorder}); then all triangles are drawn exactly on top of it. The
8897
+ * slightly shifted underlay only shows through the seam.
8898
+ */
8899
+ #drawRasterMeshes(tiles, pixelated) {
8900
+ const meshes = [];
8901
+ for (const tile of tiles) {
8902
+ if (!tile.triangles)
8903
+ continue;
8904
+ const imageId = `raster-${String(this.#rasterDefs.length)}`;
8905
+ const size = String(RASTER_TILE_UNITS);
8906
+ let imageAttrs = `id="${imageId}" width="${size}" height="${size}" preserveAspectRatio="none" xlink:href="${tile.dataUri}"`;
8907
+ if (pixelated)
8908
+ imageAttrs += ' style="image-rendering:pixelated"';
8909
+ this.#rasterDefs.push(`<image ${imageAttrs} />`);
8910
+ meshes.push({ imageId, triangles: tile.triangles });
8911
+ }
8912
+ for (const { imageId, triangles } of meshes) {
8913
+ for (const { source, target } of triangles) {
8914
+ if (!isOnTileBorder(source))
8915
+ continue;
8916
+ this.#drawRasterTriangle(imageId, bleedAtTileBorder(source, target), target);
8917
+ }
8918
+ }
8919
+ for (const { imageId, triangles } of meshes) {
8920
+ for (const { source, target } of triangles) {
8921
+ this.#drawRasterTriangle(imageId, source, target);
8922
+ }
8923
+ }
8924
+ }
8925
+ #drawRasterTriangle(imageId, source, target) {
8926
+ const matrix = affineFromTriangles(source, target);
8927
+ if (!matrix)
8928
+ return;
8929
+ const clip = growTriangle(target, RASTER_TRIANGLE_OVERLAP_PX);
8930
+ const clipId = `raster-clip-${String(this.#rasterClipCount++)}`;
8931
+ const d = segmentsToPath([clip.map(([x, y]) => roundXY(x, y))], true);
8932
+ this.#svg.push(`<clipPath id="${clipId}"><path d="${d}"/></clipPath>` +
8933
+ `<g clip-path="url(#${clipId})"><use xlink:href="#${imageId}" transform="matrix(${matrix.map(formatUnit).join(',')})" /></g>`);
8934
+ }
8779
8935
  getString() {
8780
8936
  const w = this.width.toFixed(0);
8781
8937
  const h = this.height.toFixed(0);
8782
8938
  // Build defs content
8783
- const defsContent = [`<clipPath id="vb"><rect width="${w}" height="${h}"/></clipPath>`];
8939
+ const clip = this.#clipCircle;
8940
+ const clipShape = clip
8941
+ ? `<circle cx="${formatUnit(clip.x)}" cy="${formatUnit(clip.y)}" r="${formatUnit(clip.radius)}"/>`
8942
+ : `<rect width="${w}" height="${h}"/>`;
8943
+ const defsContent = [`<clipPath id="vb">${clipShape}</clipPath>`, ...this.#rasterDefs];
8784
8944
  for (const sheet of this.#spriteSheetDefs.values()) {
8785
8945
  defsContent.push(`<image id="${escapeXml(sheet.defId)}" width="${formatNum(sheet.width)}" height="${formatNum(sheet.height)}" xlink:href="${escapeXml(sheet.href)}" />`);
8786
8946
  }
@@ -8807,9 +8967,6 @@ class SVGRenderer {
8807
8967
  `<defs>\n ${defsContent.join('\n ')}\n</defs>`,
8808
8968
  `<g id="map" clip-path="url(#vb)">`,
8809
8969
  ];
8810
- if (this.#backgroundColor.alpha > 0) {
8811
- parts.push(`<rect x="-1" y="-1" width="${(this.width + 2).toFixed(0)}" height="${(this.height + 2).toFixed(0)}" ${fillAttr(this.#backgroundColor)} />`);
8812
- }
8813
8970
  parts.push(...this.#svg, '</g>', '</svg>');
8814
8971
  return parts.join('\n');
8815
8972
  }
@@ -8829,6 +8986,85 @@ function strokeAttr(color, width) {
8829
8986
  function formatScaled(v) {
8830
8987
  return formatNum(Math.round(v * 10));
8831
8988
  }
8989
+ /** Size of a raster tile image in user units, when drawn as a mesh of triangles on the globe. */
8990
+ const RASTER_TILE_UNITS = 256;
8991
+ /** How far (pixels) a raster tile image reaches beyond the tile border in the seam underlay. */
8992
+ const RASTER_TILE_BLEED_PX = 1;
8993
+ /** How far (pixels) each raster triangle is grown beyond its edges to overlap its neighbours. */
8994
+ const RASTER_TRIANGLE_OVERLAP_PX = 0.5;
8995
+ /**
8996
+ * The affine transform [a, b, c, d, e, f] mapping the source triangle (in tile units 0..1,
8997
+ * scaled to RASTER_TILE_UNITS) exactly onto the target triangle, or undefined if either is
8998
+ * degenerate.
8999
+ */
9000
+ function affineFromTriangles(source, target) {
9001
+ const k = RASTER_TILE_UNITS;
9002
+ const [[u0, v0], [u1, v1], [u2, v2]] = source.map(([u, v]) => [u * k, v * k]);
9003
+ const [[x0, y0], [x1, y1], [x2, y2]] = target;
9004
+ const du1 = u1 - u0;
9005
+ const dv1 = v1 - v0;
9006
+ const du2 = u2 - u0;
9007
+ const dv2 = v2 - v0;
9008
+ const det = du1 * dv2 - du2 * dv1;
9009
+ if (Math.abs(det) < 1e-12)
9010
+ return undefined;
9011
+ const dx1 = x1 - x0;
9012
+ const dx2 = x2 - x0;
9013
+ const dy1 = y1 - y0;
9014
+ const dy2 = y2 - y0;
9015
+ // Screen area below a hundredth of a pixel: nothing to draw.
9016
+ if (Math.abs(dx1 * dy2 - dx2 * dy1) < 0.02)
9017
+ return undefined;
9018
+ const a = (dx1 * dv2 - dx2 * dv1) / det;
9019
+ const c = (du1 * dx2 - du2 * dx1) / det;
9020
+ const b = (dy1 * dv2 - dy2 * dv1) / det;
9021
+ const d = (du1 * dy2 - du2 * dy1) / det;
9022
+ return [a, b, c, d, x0 - a * u0 - c * v0, y0 - b * u0 - d * v0];
9023
+ }
9024
+ function isOnTileBorder(source) {
9025
+ return source.some(([u, v]) => u === 0 || u === 1 || v === 0 || v === 1);
9026
+ }
9027
+ /**
9028
+ * Moves the source corners of a triangle that lie on the tile border inwards, by the
9029
+ * equivalent of RASTER_TILE_BLEED_PX on screen, so that the image edge lies beyond the border.
9030
+ * (Used for the underlay that closes the seams between tiles, see `#drawRasterMeshes`.)
9031
+ */
9032
+ function bleedAtTileBorder(source, target) {
9033
+ const [[u0, v0], [u1, v1], [u2, v2]] = source;
9034
+ const [[x0, y0], [x1, y1], [x2, y2]] = target;
9035
+ const sourceArea = Math.abs((u1 - u0) * (v2 - v0) - (u2 - u0) * (v1 - v0));
9036
+ const targetArea = Math.abs((x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0));
9037
+ if (sourceArea === 0 || targetArea === 0)
9038
+ return source;
9039
+ // Pixels per tile unit, and the bleed in tile units, kept within the triangle.
9040
+ const scale = Math.sqrt(targetArea / sourceArea);
9041
+ const size = Math.max(Math.abs(u1 - u0), Math.abs(u2 - u0), Math.abs(v1 - v0), Math.abs(v2 - v0));
9042
+ const bleed = Math.min(RASTER_TILE_BLEED_PX / scale, size / 4);
9043
+ const inset = (t) => (t === 0 ? bleed : t === 1 ? 1 - bleed : t);
9044
+ return source.map(([u, v]) => [inset(u), inset(v)]);
9045
+ }
9046
+ /**
9047
+ * Moves every edge of a triangle outwards by `distance`, by scaling it around its incenter.
9048
+ * Growth is limited for very thin triangles, whose corners would otherwise shoot far out.
9049
+ */
9050
+ function growTriangle(triangle, distance) {
9051
+ const [[x0, y0], [x1, y1], [x2, y2]] = triangle;
9052
+ const a = Math.hypot(x2 - x1, y2 - y1);
9053
+ const b = Math.hypot(x2 - x0, y2 - y0);
9054
+ const c = Math.hypot(x1 - x0, y1 - y0);
9055
+ const perimeter = a + b + c;
9056
+ const area = Math.abs((x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0)) / 2;
9057
+ if (perimeter === 0 || area === 0)
9058
+ return triangle;
9059
+ const inradius = (2 * area) / perimeter;
9060
+ const cx = (a * x0 + b * x1 + c * x2) / perimeter;
9061
+ const cy = (a * y0 + b * y1 + c * y2) / perimeter;
9062
+ const scale = 1 + distance / Math.max(inradius, 2 * distance);
9063
+ return triangle.map(([x, y]) => [cx + (x - cx) * scale, cy + (y - cy) * scale]);
9064
+ }
9065
+ function formatUnit(v) {
9066
+ return (Math.round(v * 100000) / 100000).toString();
9067
+ }
8832
9068
  function formatScale(v) {
8833
9069
  return (Math.round(v * 10000) / 10000).toString();
8834
9070
  }
@@ -8922,6 +9158,12 @@ class Feature {
8922
9158
  properties;
8923
9159
  patterns;
8924
9160
  geometry;
9161
+ /**
9162
+ * The boundary of a polygon for the fill-antialias outline, as open polylines — when it
9163
+ * differs from the polygon's rings, i.e. when the polygon was clipped to its tile: the
9164
+ * clipped edges along the tile border are not part of the outline.
9165
+ */
9166
+ outline;
8925
9167
  #bbox;
8926
9168
  constructor(opt) {
8927
9169
  this.type = opt.type;
@@ -8929,6 +9171,7 @@ class Feature {
8929
9171
  this.properties = opt.properties;
8930
9172
  this.patterns = opt.patterns;
8931
9173
  this.geometry = opt.geometry;
9174
+ this.outline = opt.outline;
8932
9175
  }
8933
9176
  getBbox() {
8934
9177
  if (this.#bbox)
@@ -10023,6 +10266,138 @@ function readUtf8(buf, pos, end) {
10023
10266
  return str;
10024
10267
  }
10025
10268
 
10269
+ /**
10270
+ * Clipping of vector tile geometry to the tile's own square.
10271
+ *
10272
+ * Vector tiles contain a buffer: geometry reaches a little beyond the tile, so a polygon on
10273
+ * a tile border exists in both tiles. MapLibre draws each tile clipped to its square (with the
10274
+ * stencil buffer), so nothing is drawn twice. Opaque fills hide the difference, translucent
10275
+ * ones do not: the overlap would be drawn with double opacity.
10276
+ */
10277
+ /** Whether any point lies outside of the square [min, max]². */
10278
+ function exceedsSquare(rings, min, max) {
10279
+ return rings.some((ring) => ring.some(({ x, y }) => x < min || x > max || y < min || y > max));
10280
+ }
10281
+ /**
10282
+ * Clips the rings of a polygon to the square [min, max]² (Sutherland–Hodgman). Rings that
10283
+ * vanish are dropped.
10284
+ */
10285
+ function clipPolygon(rings, min, max) {
10286
+ const result = [];
10287
+ for (const ring of rings) {
10288
+ let points = ring;
10289
+ points = clipAgainst(points, (p) => p.x >= min, (a, b) => lerpAtX(a, b, min));
10290
+ points = clipAgainst(points, (p) => p.x <= max, (a, b) => lerpAtX(a, b, max));
10291
+ points = clipAgainst(points, (p) => p.y >= min, (a, b) => lerpAtY(a, b, min));
10292
+ points = clipAgainst(points, (p) => p.y <= max, (a, b) => lerpAtY(a, b, max));
10293
+ if (points.length >= 3)
10294
+ result.push(points);
10295
+ }
10296
+ return result;
10297
+ }
10298
+ /**
10299
+ * The boundary of a polygon within the square [min, max]², as open polylines: the parts of
10300
+ * its rings inside the square, without the edges the clipping adds along the square's border.
10301
+ * (MapLibre's fill outline follows the original rings, cut off at the tile border.)
10302
+ */
10303
+ function clipPolygonOutline(rings, min, max) {
10304
+ const result = [];
10305
+ for (const ring of rings) {
10306
+ if (ring.length < 2)
10307
+ continue;
10308
+ const first = ring[0];
10309
+ const last = ring[ring.length - 1];
10310
+ const closed = first.x === last.x && first.y === last.y ? ring : [...ring, first];
10311
+ result.push(...clipLine(closed, min, max));
10312
+ }
10313
+ return result;
10314
+ }
10315
+ /** Clips a polyline to the square [min, max]², returning the parts inside of it. */
10316
+ function clipLine(line, min, max) {
10317
+ const parts = [];
10318
+ let current = [];
10319
+ for (let i = 0; i + 1 < line.length; i++) {
10320
+ const segment = clipSegment(line[i], line[i + 1], min, max);
10321
+ if (!segment) {
10322
+ if (current.length > 1)
10323
+ parts.push(current);
10324
+ current = [];
10325
+ continue;
10326
+ }
10327
+ const [a, b] = segment;
10328
+ const last = current[current.length - 1];
10329
+ if (last?.x !== a.x || last.y !== a.y) {
10330
+ if (current.length > 1)
10331
+ parts.push(current);
10332
+ current = [a];
10333
+ }
10334
+ current.push(b);
10335
+ // The segment left the square: the line continues elsewhere.
10336
+ if (b !== line[i + 1]) {
10337
+ parts.push(current);
10338
+ current = [];
10339
+ }
10340
+ }
10341
+ if (current.length > 1)
10342
+ parts.push(current);
10343
+ return parts;
10344
+ }
10345
+ /** Liang–Barsky: the part of the segment a→b inside the square, or undefined. */
10346
+ function clipSegment(a, b, min, max) {
10347
+ const dx = b.x - a.x;
10348
+ const dy = b.y - a.y;
10349
+ let t0 = 0;
10350
+ let t1 = 1;
10351
+ for (const [p, q] of [
10352
+ [-dx, a.x - min],
10353
+ [dx, max - a.x],
10354
+ [-dy, a.y - min],
10355
+ [dy, max - a.y],
10356
+ ]) {
10357
+ if (p === 0) {
10358
+ if (q < 0)
10359
+ return undefined;
10360
+ continue;
10361
+ }
10362
+ const t = q / p;
10363
+ if (p < 0) {
10364
+ if (t > t1)
10365
+ return undefined;
10366
+ if (t > t0)
10367
+ t0 = t;
10368
+ }
10369
+ else {
10370
+ if (t < t0)
10371
+ return undefined;
10372
+ if (t < t1)
10373
+ t1 = t;
10374
+ }
10375
+ }
10376
+ return [
10377
+ t0 === 0 ? a : { x: a.x + dx * t0, y: a.y + dy * t0 },
10378
+ t1 === 1 ? b : { x: a.x + dx * t1, y: a.y + dy * t1 },
10379
+ ];
10380
+ }
10381
+ function clipAgainst(points, inside, intersect) {
10382
+ const result = [];
10383
+ for (let i = 0; i < points.length; i++) {
10384
+ const current = points[i];
10385
+ const previous = points[(i + points.length - 1) % points.length];
10386
+ const currentInside = inside(current);
10387
+ if (currentInside !== inside(previous))
10388
+ result.push(intersect(previous, current));
10389
+ if (currentInside)
10390
+ result.push(current);
10391
+ }
10392
+ return result;
10393
+ }
10394
+ function lerpAtX(a, b, x) {
10395
+ return { x, y: a.y + ((b.y - a.y) * (x - a.x)) / (b.x - a.x) };
10396
+ }
10397
+ function lerpAtY(a, b, y) {
10398
+ return { x: a.x + ((b.x - a.x) * (y - a.y)) / (b.y - a.y), y };
10399
+ }
10400
+
10026
10401
  const TILE_EXTENT = 4096;
10027
10402
  const VTFeatureType = { Unknown: 0, Point: 1, LineString: 2, Polygon: 3 };
10028
10403
  async function loadVectorSource(source, job, layerFeatures) {
@@ -10030,11 +10405,8 @@ async function loadVectorSource(source, job, layerFeatures) {
10030
10405
  if (!tiles)
10031
10406
  return;
10032
10407
  const { width, height } = job.renderer;
10033
- const { zoom, center } = job.view;
10034
- const { zoomLevel, tileSize, tiles: tileCoordinates, } = calculateTileGrid(width, height, center, zoom, source.maxzoom);
10035
- await Promise.all(tileCoordinates.map(async ({ x, y, offsetX, offsetY }) => {
10036
- const offset = new Point2D(offsetX, offsetY);
10037
- const tile = await getTile(tiles[0], zoomLevel, x, y);
10408
+ await Promise.all(getTileProjections(source, job).map(async ({ x, y, z, project, clipToTile }) => {
10409
+ const tile = await getTile(tiles[0], z, x, y);
10038
10410
  if (!tile)
10039
10411
  return;
10040
10412
  const vectorTile = new VectorTile(new PbfReader(tile.buffer));
@@ -10046,9 +10418,6 @@ async function loadVectorSource(source, job, layerFeatures) {
10046
10418
  }
10047
10419
  for (let i = 0; i < layer.length; i++) {
10048
10420
  const featureSrc = layer.feature(i);
10049
- const geometry = featureSrc
10050
- .loadGeometry()
10051
- .map((ring) => ring.map((point) => new Point2D(point.x, point.y).scale(tileSize / TILE_EXTENT).translate(offset)));
10052
10421
  let type;
10053
10422
  let list;
10054
10423
  switch (featureSrc.type) {
@@ -10067,6 +10436,22 @@ async function loadVectorSource(source, job, layerFeatures) {
10067
10436
  list = features.polygons;
10068
10437
  break;
10069
10438
  }
10439
+ let rings = featureSrc.loadGeometry();
10440
+ let outline;
10441
+ // Clip polygons and lines to the tile, like MapLibre's stencil clipping: otherwise
10442
+ // the parts in the tile buffer are drawn twice, which shows when translucent.
10443
+ if (clipToTile && type !== 'Point' && exceedsSquare(rings, 0, TILE_EXTENT)) {
10444
+ if (type === 'Polygon') {
10445
+ outline = project('LineString', clipPolygonOutline(rings, 0, TILE_EXTENT));
10446
+ rings = clipPolygon(rings, 0, TILE_EXTENT);
10447
+ }
10448
+ else {
10449
+ rings = rings.flatMap((line) => clipLine(line, 0, TILE_EXTENT));
10450
+ }
10451
+ }
10452
+ const geometry = project(type, rings);
10453
+ if (geometry.length === 0)
10454
+ continue;
10070
10455
  // Split MultiPoint into individual Point features
10071
10456
  if (type === 'Point' && geometry.length > 1) {
10072
10457
  for (const ring of geometry) {
@@ -10084,6 +10469,7 @@ async function loadVectorSource(source, job, layerFeatures) {
10084
10469
  const feature = new Feature({
10085
10470
  type,
10086
10471
  geometry,
10472
+ outline,
10087
10473
  id: featureSrc.id,
10088
10474
  properties: featureSrc.properties,
10089
10475
  });
@@ -10094,9 +10480,41 @@ async function loadVectorSource(source, job, layerFeatures) {
10094
10480
  }
10095
10481
  }));
10096
10482
  }
10483
+ function getTileProjections(source, job) {
10484
+ const { width, height } = job.renderer;
10485
+ const { zoom, center } = job.view;
10486
+ const projection = job.projection;
10487
+ if (projection?.isGlobe) {
10488
+ // Never zoom level 0 on the globe: the left and right edge of its single tile coincide
10489
+ // (the antimeridian), which the horizon clipping cannot tell apart.
10490
+ const z = Math.max(1, Math.min(Math.floor(zoom), source.maxzoom ?? Infinity));
10491
+ const scale = 1 / (TILE_EXTENT * 2 ** z);
10492
+ return projection.coveringTiles(z).map(({ x, y }) => ({
10493
+ x,
10494
+ y,
10495
+ z,
10496
+ clipToTile: projection.isTileFullyVisible({ x, y, z }),
10497
+ project: (type, rings) => projection.projectGeometry(type, rings.map((ring) => ring.map((p) => [
10498
+ (x * TILE_EXTENT + p.x) * scale,
10499
+ (y * TILE_EXTENT + p.y) * scale,
10500
+ ]))),
10501
+ }));
10502
+ }
10503
+ const { zoomLevel, tileSize, tiles } = calculateTileGrid(width, height, center, zoom, source.maxzoom);
10504
+ return tiles.map(({ x, y, offsetX, offsetY }) => {
10505
+ const offset = new Point2D(offsetX, offsetY);
10506
+ return {
10507
+ x,
10508
+ y,
10509
+ z: zoomLevel,
10510
+ clipToTile: true,
10511
+ project: (_type, rings) => rings.map((ring) => ring.map((point) => new Point2D(point.x, point.y).scale(tileSize / TILE_EXTENT).translate(offset))),
10512
+ };
10513
+ });
10514
+ }
10097
10515
 
10098
10516
  function loadGeoJSONSource(options) {
10099
- const { sourceName, data, width, height, zoom, center, layerFeatures } = options;
10517
+ const { sourceName, data, width, height, zoom, center, layerFeatures, projection } = options;
10100
10518
  const existing = layerFeatures.get(sourceName);
10101
10519
  const features = existing ?? {
10102
10520
  points: [],
@@ -10109,9 +10527,17 @@ function loadGeoJSONSource(options) {
10109
10527
  layerFeatures.set(sourceName, features);
10110
10528
  const worldSize = 512 * 2 ** zoom;
10111
10529
  const centerMercator = new Point2D(center[0], center[1]).getProject2Pixel();
10112
- function projectCoord(coord) {
10530
+ function toMercator(coord) {
10113
10531
  const mercator = new Point2D(coord[0], coord[1]).getProject2Pixel();
10114
- return new Point2D((mercator.x - centerMercator.x) * worldSize + width / 2, (mercator.y - centerMercator.y) * worldSize + height / 2);
10532
+ return [mercator.x, mercator.y];
10533
+ }
10534
+ function project(type, rings) {
10535
+ if (projection?.isGlobe) {
10536
+ if (type === 'Polygon')
10537
+ rings = orientRings(rings);
10538
+ return projection.projectGeometry(type, rings);
10539
+ }
10540
+ return rings.map((ring) => ring.map(([x, y]) => new Point2D((x - centerMercator.x) * worldSize + width / 2, (y - centerMercator.y) * worldSize + height / 2)));
10115
10541
  }
10116
10542
  function makeFeature(type, geometry, id, properties) {
10117
10543
  const feature = new Feature({ type, geometry, id, properties });
@@ -10122,7 +10548,10 @@ function loadGeoJSONSource(options) {
10122
10548
  function extractPoints(geometry) {
10123
10549
  return geometry.flatMap((ring) => ring.map((p) => [p]));
10124
10550
  }
10125
- function addFeature(type, geometry, id, properties) {
10551
+ function addFeature(type, rings, id, properties) {
10552
+ const geometry = project(type, rings);
10553
+ if (geometry.length === 0)
10554
+ return;
10126
10555
  switch (type) {
10127
10556
  case 'Point': {
10128
10557
  const f = makeFeature('Point', geometry, id, properties);
@@ -10165,23 +10594,23 @@ function loadGeoJSONSource(options) {
10165
10594
  function processGeometry(geom, id, properties) {
10166
10595
  switch (geom.type) {
10167
10596
  case 'Point':
10168
- addFeature('Point', [[projectCoord(geom.coordinates)]], id, properties);
10597
+ addFeature('Point', [[toMercator(geom.coordinates)]], id, properties);
10169
10598
  break;
10170
10599
  case 'MultiPoint':
10171
- addFeature('Point', geom.coordinates.map((c) => [projectCoord(c)]), id, properties);
10600
+ addFeature('Point', geom.coordinates.map((c) => [toMercator(c)]), id, properties);
10172
10601
  break;
10173
10602
  case 'LineString':
10174
- addFeature('LineString', [geom.coordinates.map((c) => projectCoord(c))], id, properties);
10603
+ addFeature('LineString', [geom.coordinates.map((c) => toMercator(c))], id, properties);
10175
10604
  break;
10176
10605
  case 'MultiLineString':
10177
- addFeature('LineString', geom.coordinates.map((line) => line.map((c) => projectCoord(c))), id, properties);
10606
+ addFeature('LineString', geom.coordinates.map((line) => line.map((c) => toMercator(c))), id, properties);
10178
10607
  break;
10179
10608
  case 'Polygon':
10180
- addFeature('Polygon', geom.coordinates.map((ring) => ring.map((c) => projectCoord(c))), id, properties);
10609
+ addFeature('Polygon', geom.coordinates.map((ring) => ring.map((c) => toMercator(c))), id, properties);
10181
10610
  break;
10182
10611
  case 'MultiPolygon':
10183
10612
  for (const polygon of geom.coordinates) {
10184
- addFeature('Polygon', polygon.map((ring) => ring.map((c) => projectCoord(c))), id, properties);
10613
+ addFeature('Polygon', polygon.map((ring) => ring.map((c) => toMercator(c))), id, properties);
10185
10614
  }
10186
10615
  break;
10187
10616
  case 'GeometryCollection':
@@ -10205,6 +10634,22 @@ function loadGeoJSONSource(options) {
10205
10634
  break;
10206
10635
  }
10207
10636
  }
10637
+ /**
10638
+ * Orients polygon rings like vector tiles do (in mercator space, y pointing south: the
10639
+ * exterior ring clockwise, holes counter-clockwise), as the globe clipping relies on it.
10640
+ */
10641
+ function orientRings(rings) {
10642
+ return rings.map((ring, index) => {
10643
+ let area = 0;
10644
+ for (let i = 0; i < ring.length; i++) {
10645
+ const [x0, y0] = ring[i];
10646
+ const [x1, y1] = ring[(i + 1) % ring.length];
10647
+ area += x0 * y1 - x1 * y0;
10648
+ }
10649
+ const isExterior = index === 0;
10650
+ return area > 0 === isExterior ? ring : [...ring].reverse();
10651
+ });
10652
+ }
10208
10653
 
10209
10654
  /**
10210
10655
  * Base64-encode an ArrayBuffer (standard alphabet, with padding).
@@ -10242,6 +10687,23 @@ async function getRasterTiles(job, sourceName) {
10242
10687
  throw Error(`Invalid raster source "${sourceName}": expected type "raster" with a "tiles" array`);
10243
10688
  }
10244
10689
  const sourceUrl = source.tiles[0];
10690
+ const projection = job.projection;
10691
+ if (projection?.isGlobe) {
10692
+ // Never zoom level 0 on the globe: the left and right edge of its single tile coincide
10693
+ // (the antimeridian), which the horizon clipping cannot tell apart.
10694
+ const z = Math.max(1, Math.min(Math.floor(zoom), source.maxzoom ?? Infinity));
10695
+ const globeTiles = await Promise.all(projection.coveringTiles(z).map(async (id) => {
10696
+ const tile = await getTile(sourceUrl, id.z, id.x, id.y);
10697
+ if (!tile)
10698
+ return null;
10699
+ const triangles = projection.rasterTriangles(id);
10700
+ if (triangles.length === 0)
10701
+ return null;
10702
+ const dataUri = `data:${tile.contentType};base64,${arrayBufferToBase64(tile.buffer)}`;
10703
+ return { x: 0, y: 0, width: 1, height: 1, dataUri, triangles };
10704
+ }));
10705
+ return globeTiles.filter((tile) => tile !== null);
10706
+ }
10245
10707
  const { zoomLevel, tileSize, tiles } = calculateTileGrid(width, height, center, zoom, source.maxzoom);
10246
10708
  const rasterTiles = await Promise.all(tiles.map(async ({ x, y, offsetX, offsetY }) => {
10247
10709
  const tile = await getTile(sourceUrl, zoomLevel, x, y);
@@ -10345,6 +10807,7 @@ async function getLayerFeatures(job) {
10345
10807
  zoom,
10346
10808
  center,
10347
10809
  layerFeatures,
10810
+ projection: job.projection,
10348
10811
  });
10349
10812
  }
10350
10813
  break;
@@ -10476,6 +10939,594 @@ function getLayerStyles(layers) {
10476
10939
  return layers.map(createStyleLayer);
10477
10940
  }
10478
10941
 
10942
+ /**
10943
+ * Map projection: web mercator, MapLibre's globe ("vertical-perspective") or a blend of both.
10944
+ *
10945
+ * The math follows MapLibre GL JS (`vertical_perspective_transform.ts` and the
10946
+ * `_projection_globe.vertex.glsl` shader) for a camera without pitch, bearing or padding:
10947
+ * the globe is a sphere of radius `worldSize / (2π·cos(centerLat))` (so the map center has
10948
+ * the same scale as in mercator), viewed by a perspective camera `1.5 · height` pixels above
10949
+ * the map center. The globe → mercator transition blends the homogeneous clip-space positions
10950
+ * of both projections (x, y and w) *before* the perspective divide, exactly like the shader.
10951
+ *
10952
+ * All inputs are mercator world coordinates in the range 0..1 (`mx` east, `my` south).
10953
+ */
10954
+ /** MapLibre's default vertical field of view (`TransformHelper._fovInRadians`). */
10955
+ const FOV = 0.6435011087932844;
10956
+ /** What MapLibre expands `projection: { type: 'globe' }` into (`projection_factory.ts`). */
10957
+ const GLOBE_PRESET = [
10958
+ 'interpolate',
10959
+ ['linear'],
10960
+ ['zoom'],
10961
+ 11,
10962
+ 'vertical-perspective',
10963
+ 12,
10964
+ 'mercator',
10965
+ ];
10966
+ /** Below this globeness the far side of the globe is not clipped (MapLibre's `z_globeness_threshold`). */
10967
+ const CLIP_THRESHOLD = 0.2;
10968
+ /** Maximum distance (pixels) a straight segment may deviate from the curved globe surface. */
10969
+ const MAX_CURVE_ERROR_PX = 0.25;
10970
+ /** The same for the affine-mapped triangles of raster tiles (a coarser mesh keeps the SVG small). */
10971
+ const MAX_RASTER_ERROR_PX = 1;
10972
+ /** Upper bound for the number of cells per raster tile side. */
10973
+ const MAX_RASTER_CELLS = 16;
10974
+ /** Angle to go clockwise on screen (decreasing angle in the y-up view frame) from a0 to a1, in [0, 2π). */
10975
+ function clockwiseDelta(a0, a1) {
10976
+ const delta = (a0 - a1) % (2 * Math.PI);
10977
+ return delta < 0 ? delta + 2 * Math.PI : delta;
10978
+ }
10979
+ /**
10980
+ * Returns how "globe-like" the projection is at the given zoom, matching MapLibre's
10981
+ * `GlobeProjection.transitionState`: 0 = mercator, 1 = globe, in between = transition.
10982
+ */
10983
+ function getGlobeness(projection, zoom) {
10984
+ return evaluateGlobeness(projection?.type ?? 'mercator', zoom);
10985
+ }
10986
+ function evaluateGlobeness(value, zoom) {
10987
+ if (typeof value === 'string') {
10988
+ switch (value) {
10989
+ case 'globe':
10990
+ return evaluateGlobeness(GLOBE_PRESET, zoom);
10991
+ case 'vertical-perspective':
10992
+ return 1;
10993
+ default:
10994
+ return 0;
10995
+ }
10996
+ }
10997
+ if (!Array.isArray(value))
10998
+ return 0;
10999
+ const [op, ...args] = value;
11000
+ if (op === 'literal')
11001
+ return evaluateGlobeness(args[0], zoom);
11002
+ if (op === 'step') {
11003
+ // ["step", ["zoom"], output0, stop1, output1, ...]
11004
+ let result = args[1];
11005
+ for (let i = 2; i + 1 < args.length; i += 2) {
11006
+ if (zoom >= args[i])
11007
+ result = args[i + 1];
11008
+ }
11009
+ return evaluateGlobeness(result, zoom);
11010
+ }
11011
+ if (op === 'interpolate') {
11012
+ // ["interpolate", ["linear"] | ["exponential", base], ["zoom"], stop0, output0, ...]
11013
+ const interpolation = args[0];
11014
+ const base = interpolation[0] === 'exponential' ? interpolation[1] : 1;
11015
+ const stops = [];
11016
+ for (let i = 2; i + 1 < args.length; i += 2)
11017
+ stops.push([args[i], args[i + 1]]);
11018
+ if (stops.length === 0)
11019
+ return 0;
11020
+ if (zoom <= stops[0][0])
11021
+ return evaluateGlobeness(stops[0][1], zoom);
11022
+ for (let i = 1; i < stops.length; i++) {
11023
+ const [z1, out1] = stops[i];
11024
+ if (zoom > z1)
11025
+ continue;
11026
+ const [z0, out0] = stops[i - 1];
11027
+ const t = interpolationFactor(zoom, z0, z1, base);
11028
+ const g0 = evaluateGlobeness(out0, zoom);
11029
+ const g1 = evaluateGlobeness(out1, zoom);
11030
+ return g0 + (g1 - g0) * t;
11031
+ }
11032
+ return evaluateGlobeness(stops[stops.length - 1][1], zoom);
11033
+ }
11034
+ // A projection transition literal: [from, to, transition]
11035
+ if (value.length === 3 && typeof value[2] === 'number') {
11036
+ const g0 = evaluateGlobeness(value[0], zoom);
11037
+ const g1 = evaluateGlobeness(value[1], zoom);
11038
+ return g0 + (g1 - g0) * value[2];
11039
+ }
11040
+ return 0;
11041
+ }
11042
+ function interpolationFactor(input, lower, upper, base) {
11043
+ const difference = upper - lower;
11044
+ if (difference === 0)
11045
+ return 0;
11046
+ const progress = input - lower;
11047
+ if (base === 1)
11048
+ return progress / difference;
11049
+ return (Math.pow(base, progress) - 1) / (Math.pow(base, difference) - 1);
11050
+ }
11051
+ class Projection {
11052
+ width;
11053
+ height;
11054
+ zoom;
11055
+ /** 0 = mercator, 1 = globe, in between = transition (see {@link getGlobeness}). */
11056
+ globeness;
11057
+ /** Size of the whole world in pixels at the current zoom (mercator). */
11058
+ worldSize;
11059
+ /**
11060
+ * Maximum length (in mercator units) of a straight segment before it has to be
11061
+ * subdivided to follow the curvature of the globe.
11062
+ */
11063
+ maxSegmentLength;
11064
+ /** Whether geometry on the far side of the globe has to be removed. */
11065
+ clipsHorizon;
11066
+ #centerX;
11067
+ #centerY;
11068
+ #centerLng;
11069
+ #sinLat;
11070
+ #cosLat;
11071
+ /** Globe radius in pixels. */
11072
+ #radius;
11073
+ /** Distance of the camera to the map center in pixels. */
11074
+ #cameraDistance;
11075
+ /** Points of the unit sphere with a view-space z below this are behind the horizon. */
11076
+ #horizonZ;
11077
+ constructor(opt) {
11078
+ this.width = opt.width;
11079
+ this.height = opt.height;
11080
+ this.zoom = opt.zoom;
11081
+ this.globeness = Math.min(1, Math.max(0, opt.globeness ?? 0));
11082
+ this.worldSize = 512 * 2 ** opt.zoom;
11083
+ const center = new Point2D(opt.center[0], opt.center[1]).getProject2Pixel();
11084
+ this.#centerX = center.x;
11085
+ this.#centerY = center.y;
11086
+ const lat = (opt.center[1] * Math.PI) / 180;
11087
+ this.#centerLng = (opt.center[0] * Math.PI) / 180;
11088
+ this.#sinLat = Math.sin(lat);
11089
+ this.#cosLat = Math.cos(lat);
11090
+ this.#radius = this.worldSize / (2 * Math.PI * this.#cosLat);
11091
+ this.#cameraDistance = this.height / 2 / Math.tan(FOV / 2);
11092
+ this.#horizonZ = this.#radius / (this.#radius + this.#cameraDistance);
11093
+ this.clipsHorizon = this.globeness > CLIP_THRESHOLD;
11094
+ // A chord spanning the angle θ deviates R·θ²/8 from the sphere.
11095
+ const maxAngle = Math.sqrt((8 * MAX_CURVE_ERROR_PX) / this.#radius);
11096
+ this.maxSegmentLength = maxAngle / (2 * Math.PI);
11097
+ }
11098
+ static fromStyle(opt) {
11099
+ return new Projection({ ...opt, globeness: getGlobeness(opt.projection, opt.zoom) });
11100
+ }
11101
+ /** True when the globe projection is (at least partially) in effect. */
11102
+ get isGlobe() {
11103
+ return this.globeness > 0;
11104
+ }
11105
+ /**
11106
+ * The silhouette of the globe on screen, if the map has to be clipped to it.
11107
+ * (During the globe → mercator transition the silhouette is far outside the screen.)
11108
+ */
11109
+ get clipCircle() {
11110
+ if (!this.clipsHorizon)
11111
+ return undefined;
11112
+ const r = this.#radius;
11113
+ const d = this.#cameraDistance;
11114
+ const radius = (d * r) / Math.sqrt((d + r) * (d + r) - r * r);
11115
+ return { x: this.width / 2, y: this.height / 2, radius };
11116
+ }
11117
+ /** Mercator world coordinates → unit sphere in the view frame (x right, y up, z towards the camera). */
11118
+ toSphere(mx, my) {
11119
+ const dLng = mx * 2 * Math.PI - Math.PI - this.#centerLng;
11120
+ const lat = 2 * Math.atan(Math.exp(Math.PI - my * 2 * Math.PI)) - Math.PI / 2;
11121
+ const cosLat = Math.cos(lat);
11122
+ const x = Math.sin(dLng) * cosLat;
11123
+ const y = Math.sin(lat);
11124
+ const z = Math.cos(dLng) * cosLat;
11125
+ return [x, y * this.#cosLat - z * this.#sinLat, y * this.#sinLat + z * this.#cosLat];
11126
+ }
11127
+ /** Inverse of {@link toSphere}. */
11128
+ fromSphere(v) {
11129
+ const y = v[1] * this.#cosLat + v[2] * this.#sinLat;
11130
+ const z = -v[1] * this.#sinLat + v[2] * this.#cosLat;
11131
+ const lat = Math.asin(Math.max(-1, Math.min(1, y)));
11132
+ const lng = Math.atan2(v[0], z) + this.#centerLng;
11133
+ const mx = (lng + Math.PI) / (2 * Math.PI);
11134
+ const my = (Math.PI - Math.log(Math.tan(Math.PI / 4 + lat / 2))) / (2 * Math.PI);
11135
+ return [mx - Math.floor(mx), my];
11136
+ }
11137
+ isVisible(v) {
11138
+ return !this.clipsHorizon || v[2] >= this.#horizonZ;
11139
+ }
11140
+ /** Screen position of a mercator point. */
11141
+ project(mx, my, v) {
11142
+ // Flat (mercator) position, relative to the screen center. Use the world copy
11143
+ // nearest to the center.
11144
+ let dx = mx - this.#centerX;
11145
+ dx -= Math.round(dx);
11146
+ const flatX = dx * this.worldSize;
11147
+ const flatY = (my - this.#centerY) * this.worldSize;
11148
+ const t = this.globeness;
11149
+ if (t === 0)
11150
+ return new Point2D(flatX + this.width / 2, flatY + this.height / 2);
11151
+ v ??= this.toSphere(mx, my);
11152
+ const d = this.#cameraDistance;
11153
+ const r = this.#radius;
11154
+ // Homogeneous coordinates (x, y, w) of the globe position; the flat one is (d·x, d·y, d).
11155
+ const globeX = d * r * v[0];
11156
+ const globeY = -d * r * v[1];
11157
+ const globeW = d + r * (1 - v[2]);
11158
+ const w = d + (globeW - d) * t;
11159
+ return new Point2D((d * flatX + (globeX - d * flatX) * t) / w + this.width / 2, (d * flatY + (globeY - d * flatY) * t) / w + this.height / 2);
11160
+ }
11161
+ /**
11162
+ * Projects feature geometry given in mercator world coordinates to screen pixels.
11163
+ * On the globe, segments are subdivided to follow the curvature, lines are cut at
11164
+ * the horizon and polygons are clipped to the visible hemisphere (the clipped part of
11165
+ * the boundary follows the globe's silhouette).
11166
+ */
11167
+ projectGeometry(type, rings) {
11168
+ if (!this.isGlobe)
11169
+ return rings.map((ring) => ring.map(([mx, my]) => this.project(mx, my)));
11170
+ switch (type) {
11171
+ case 'Point':
11172
+ return rings
11173
+ .map((ring) => ring.flatMap(([mx, my]) => {
11174
+ const v = this.toSphere(mx, my);
11175
+ return this.isVisible(v) ? [this.project(mx, my, v)] : [];
11176
+ }))
11177
+ .filter((ring) => ring.length > 0);
11178
+ case 'LineString':
11179
+ return rings.flatMap((ring) => this.#clipLine(this.#densify(ring, false)));
11180
+ case 'Polygon':
11181
+ return this.#clipPolygon(rings.map((ring) => this.#densify(ring, true)));
11182
+ }
11183
+ }
11184
+ /** Subdivides long segments (in mercator space) and lifts the points onto the sphere. */
11185
+ #densify(ring, closed) {
11186
+ const maxLength = this.maxSegmentLength;
11187
+ const result = [];
11188
+ const count = ring.length;
11189
+ const segments = closed ? count : count - 1;
11190
+ for (let i = 0; i < count; i++) {
11191
+ const [x0, y0] = ring[i];
11192
+ result.push({ mx: x0, my: y0, v: this.toSphere(x0, y0) });
11193
+ if (i >= segments)
11194
+ continue;
11195
+ const [x1, y1] = ring[(i + 1) % count];
11196
+ const steps = Math.ceil(Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0)) / maxLength);
11197
+ for (let s = 1; s < steps; s++) {
11198
+ const mx = x0 + ((x1 - x0) * s) / steps;
11199
+ const my = y0 + ((y1 - y0) * s) / steps;
11200
+ result.push({ mx, my, v: this.toSphere(mx, my) });
11201
+ }
11202
+ }
11203
+ return result;
11204
+ }
11205
+ /** Point on the segment a→b where it crosses the horizon plane. */
11206
+ #horizonCrossing(a, b) {
11207
+ const s = (a.v[2] - this.#horizonZ) / (a.v[2] - b.v[2]);
11208
+ const mx = a.mx + (b.mx - a.mx) * s;
11209
+ const my = a.my + (b.my - a.my) * s;
11210
+ return { mx, my, v: this.toSphere(mx, my) };
11211
+ }
11212
+ /** Splits a line into its visible parts. */
11213
+ #clipLine(points) {
11214
+ if (!this.clipsHorizon)
11215
+ return [points.map((p) => this.project(p.mx, p.my, p.v))];
11216
+ const parts = [];
11217
+ let current = [];
11218
+ for (let i = 0; i < points.length; i++) {
11219
+ const p = points[i];
11220
+ const visible = this.isVisible(p.v);
11221
+ const prev = i > 0 ? points[i - 1] : undefined;
11222
+ if (prev && this.isVisible(prev.v) !== visible) {
11223
+ const c = this.#horizonCrossing(prev, p);
11224
+ current.push(this.project(c.mx, c.my, c.v));
11225
+ if (!visible) {
11226
+ parts.push(current);
11227
+ current = [];
11228
+ }
11229
+ }
11230
+ if (visible)
11231
+ current.push(this.project(p.mx, p.my, p.v));
11232
+ }
11233
+ if (current.length > 0)
11234
+ parts.push(current);
11235
+ return parts.filter((part) => part.length > 1);
11236
+ }
11237
+ /**
11238
+ * Clips the rings of a polygon to the visible hemisphere.
11239
+ *
11240
+ * Every ring crossing the horizon is cut into its visible chains, each running from an
11241
+ * entry point to an exit point on the horizon. The chains are then reconnected along the
11242
+ * silhouette: from an exit point, the silhouette is followed to the nearest entry point
11243
+ * (of any chain of any ring). Rings follow the vector tile winding (exterior rings
11244
+ * clockwise on screen, holes counter-clockwise), which keeps the filled area on the right
11245
+ * of every ring, so the silhouette is always walked clockwise.
11246
+ */
11247
+ #clipPolygon(rings) {
11248
+ const project = (p) => this.project(p.mx, p.my, p.v);
11249
+ if (!this.clipsHorizon)
11250
+ return rings.map((ring) => ring.map(project));
11251
+ const result = [];
11252
+ const chains = [];
11253
+ const angleOf = (p) => Math.atan2(p.v[1], p.v[0]);
11254
+ for (const ring of rings) {
11255
+ const count = ring.length;
11256
+ const visible = ring.map((p) => this.isVisible(p.v));
11257
+ // Start walking at an entry point, so every chain is complete.
11258
+ let start = -1;
11259
+ for (let i = 0; i < count; i++) {
11260
+ if (visible[i] && !visible[(i + count - 1) % count]) {
11261
+ start = i;
11262
+ break;
11263
+ }
11264
+ }
11265
+ if (start < 0) {
11266
+ // No crossing: the ring is either completely visible or completely hidden.
11267
+ if (visible[0])
11268
+ result.push(ring.map(project));
11269
+ continue;
11270
+ }
11271
+ let chain;
11272
+ for (let k = 0; k < count; k++) {
11273
+ const i = (start + k) % count;
11274
+ const p = ring[i];
11275
+ const prev = ring[(i + count - 1) % count];
11276
+ if (visible[i]) {
11277
+ if (!chain) {
11278
+ const c = this.#horizonCrossing(prev, p);
11279
+ chain = { entry: angleOf(c), exit: 0, points: [project(c)] };
11280
+ }
11281
+ chain.points.push(project(p));
11282
+ }
11283
+ else if (chain) {
11284
+ const c = this.#horizonCrossing(prev, p);
11285
+ chain.points.push(project(c));
11286
+ chain.exit = angleOf(c);
11287
+ chains.push(chain);
11288
+ chain = undefined;
11289
+ }
11290
+ }
11291
+ }
11292
+ // Reconnect the chains along the silhouette.
11293
+ const used = new Set();
11294
+ for (const first of chains) {
11295
+ if (used.has(first))
11296
+ continue;
11297
+ const ring = [];
11298
+ let chain = first;
11299
+ for (;;) {
11300
+ used.add(chain);
11301
+ ring.push(...chain.points);
11302
+ let next = first;
11303
+ let bestDelta = clockwiseDelta(chain.exit, first.entry);
11304
+ for (const candidate of chains) {
11305
+ if (used.has(candidate))
11306
+ continue;
11307
+ const delta = clockwiseDelta(chain.exit, candidate.entry);
11308
+ if (delta < bestDelta) {
11309
+ bestDelta = delta;
11310
+ next = candidate;
11311
+ }
11312
+ }
11313
+ this.#pushArc(ring, chain.exit, next.entry);
11314
+ if (next === first)
11315
+ break;
11316
+ chain = next;
11317
+ }
11318
+ result.push(ring);
11319
+ }
11320
+ return result;
11321
+ }
11322
+ /**
11323
+ * Appends points along the horizon circle, clockwise on screen (decreasing angle in the
11324
+ * y-up view frame) from angle a0 to a1.
11325
+ */
11326
+ #pushArc(result, a0, a1) {
11327
+ const delta = clockwiseDelta(a0, a1);
11328
+ const steps = Math.ceil(delta / (Math.PI / 90));
11329
+ const h = this.#horizonZ;
11330
+ const rho = Math.sqrt(1 - h * h);
11331
+ for (let s = 1; s < steps; s++) {
11332
+ const a = a0 - (delta * s) / steps;
11333
+ const v = [rho * Math.cos(a), rho * Math.sin(a), h];
11334
+ const [mx, my] = this.fromSphere(v);
11335
+ result.push(this.project(mx, my, v));
11336
+ }
11337
+ }
11338
+ /**
11339
+ * Splits a raster tile into a mesh of triangles, each small enough to be drawn with an
11340
+ * affine transform (see {@link RasterTriangle}). An affine transform is fully determined
11341
+ * by three points, so every triangle meets its projected corners exactly and neighbouring
11342
+ * triangles share their edges. Cells on the far side of the globe are dropped.
11343
+ */
11344
+ rasterTriangles(tile) {
11345
+ const tileSize = 1 / 2 ** tile.z;
11346
+ const maxLength = this.maxSegmentLength * Math.sqrt(MAX_RASTER_ERROR_PX / MAX_CURVE_ERROR_PX);
11347
+ const n = Math.max(1, Math.min(MAX_RASTER_CELLS, Math.ceil(tileSize / maxLength)));
11348
+ // Project the (n+1)² grid corners once.
11349
+ const corners = [];
11350
+ for (let j = 0; j <= n; j++) {
11351
+ for (let i = 0; i <= n; i++) {
11352
+ const mx = (tile.x + i / n) * tileSize;
11353
+ const my = (tile.y + j / n) * tileSize;
11354
+ let v = this.toSphere(mx, my);
11355
+ if (this.isVisible(v)) {
11356
+ corners.push(this.project(mx, my, v));
11357
+ }
11358
+ else {
11359
+ // Pull the corner onto the horizon, so cells at the silhouette end there.
11360
+ if (this.#isBehind(v)) {
11361
+ corners.push(undefined);
11362
+ continue;
11363
+ }
11364
+ v = this.#clampToHorizon(v);
11365
+ const [cx, cy] = this.fromSphere(v);
11366
+ corners.push(this.project(cx, cy, v));
11367
+ }
11368
+ }
11369
+ }
11370
+ const triangles = [];
11371
+ for (let j = 0; j < n; j++) {
11372
+ for (let i = 0; i < n; i++) {
11373
+ const p00 = corners[j * (n + 1) + i];
11374
+ const p10 = corners[j * (n + 1) + i + 1];
11375
+ const p01 = corners[(j + 1) * (n + 1) + i];
11376
+ const p11 = corners[(j + 1) * (n + 1) + i + 1];
11377
+ if (!p00 || !p10 || !p01 || !p11)
11378
+ continue;
11379
+ const minX = Math.min(p00.x, p10.x, p01.x, p11.x);
11380
+ const maxX = Math.max(p00.x, p10.x, p01.x, p11.x);
11381
+ const minY = Math.min(p00.y, p10.y, p01.y, p11.y);
11382
+ const maxY = Math.max(p00.y, p10.y, p01.y, p11.y);
11383
+ if (maxX < 0 || minX > this.width || maxY < 0 || minY > this.height)
11384
+ continue;
11385
+ if (!this.#isCellVisible(tile, n, i, j))
11386
+ continue;
11387
+ const u0 = i / n;
11388
+ const v0 = j / n;
11389
+ const u1 = (i + 1) / n;
11390
+ const v1 = (j + 1) / n;
11391
+ triangles.push({
11392
+ source: [
11393
+ [u0, v0],
11394
+ [u1, v0],
11395
+ [u1, v1],
11396
+ ],
11397
+ target: [
11398
+ [p00.x, p00.y],
11399
+ [p10.x, p10.y],
11400
+ [p11.x, p11.y],
11401
+ ],
11402
+ }, {
11403
+ source: [
11404
+ [u0, v0],
11405
+ [u1, v1],
11406
+ [u0, v1],
11407
+ ],
11408
+ target: [
11409
+ [p00.x, p00.y],
11410
+ [p11.x, p11.y],
11411
+ [p01.x, p01.y],
11412
+ ],
11413
+ });
11414
+ }
11415
+ }
11416
+ return triangles;
11417
+ }
11418
+ /** Whether any part of a raster cell is on the visible side of the globe. */
11419
+ #isCellVisible(tile, n, i, j) {
11420
+ if (!this.clipsHorizon)
11421
+ return true;
11422
+ const tileSize = 1 / 2 ** tile.z;
11423
+ for (let dj = 0; dj <= 2; dj++) {
11424
+ for (let di = 0; di <= 2; di++) {
11425
+ const mx = (tile.x + (i + di / 2) / n) * tileSize;
11426
+ const my = (tile.y + (j + dj / 2) / n) * tileSize;
11427
+ if (this.isVisible(this.toSphere(mx, my)))
11428
+ return true;
11429
+ }
11430
+ }
11431
+ return false;
11432
+ }
11433
+ /** Moves a point on the far side of the globe onto the horizon (keeping its direction on screen). */
11434
+ #clampToHorizon(v) {
11435
+ const h = this.#horizonZ;
11436
+ const rho = Math.sqrt(1 - h * h);
11437
+ const len = Math.hypot(v[0], v[1]);
11438
+ if (len < 1e-12)
11439
+ return [rho, 0, h];
11440
+ return [(v[0] / len) * rho, (v[1] / len) * rho, h];
11441
+ }
11442
+ /** Points almost opposite the view direction have no meaningful place on the horizon. */
11443
+ #isBehind(v) {
11444
+ return Math.hypot(v[0], v[1]) < 1e-9;
11445
+ }
11446
+ /**
11447
+ * Tiles at zoom level `z` that are visible on screen, found by walking down the tile
11448
+ * pyramid and projecting a grid of sample points of each tile.
11449
+ */
11450
+ coveringTiles(z) {
11451
+ const result = [];
11452
+ const visit = (tz, tx, ty) => {
11453
+ if (tz >= 2 && !this.#isTileVisible(tz, tx, ty))
11454
+ return;
11455
+ if (tz === z) {
11456
+ result.push({ x: tx, y: ty, z: tz });
11457
+ return;
11458
+ }
11459
+ for (let dy = 0; dy < 2; dy++) {
11460
+ for (let dx = 0; dx < 2; dx++)
11461
+ visit(tz + 1, tx * 2 + dx, ty * 2 + dy);
11462
+ }
11463
+ };
11464
+ visit(0, 0, 0);
11465
+ return result;
11466
+ }
11467
+ /**
11468
+ * Whether a tile, including a margin around it (the tile buffer, as a fraction of the tile
11469
+ * size), lies completely on the visible side of the globe, so no horizon clipping happens.
11470
+ */
11471
+ isTileFullyVisible(tile, margin = 0.1) {
11472
+ if (!this.clipsHorizon)
11473
+ return true;
11474
+ const size = 1 / 2 ** tile.z;
11475
+ const samples = 8;
11476
+ for (let j = 0; j <= samples; j++) {
11477
+ for (let i = 0; i <= samples; i++) {
11478
+ if (i !== 0 && i !== samples && j !== 0 && j !== samples)
11479
+ continue; // the border only
11480
+ const mx = (tile.x - margin + ((1 + 2 * margin) * i) / samples) * size;
11481
+ const my = (tile.y - margin + ((1 + 2 * margin) * j) / samples) * size;
11482
+ if (!this.isVisible(this.toSphere(mx, my)))
11483
+ return false;
11484
+ }
11485
+ }
11486
+ return true;
11487
+ }
11488
+ #isTileVisible(z, x, y) {
11489
+ const samples = 6;
11490
+ const size = 1 / 2 ** z;
11491
+ let minX = Infinity;
11492
+ let minY = Infinity;
11493
+ let maxX = -Infinity;
11494
+ let maxY = -Infinity;
11495
+ const addSample = (mx, my) => {
11496
+ const v = this.toSphere(mx, my);
11497
+ if (!this.isVisible(v))
11498
+ return;
11499
+ const p = this.project(mx, my, v);
11500
+ if (p.x < minX)
11501
+ minX = p.x;
11502
+ if (p.y < minY)
11503
+ minY = p.y;
11504
+ if (p.x > maxX)
11505
+ maxX = p.x;
11506
+ if (p.y > maxY)
11507
+ maxY = p.y;
11508
+ };
11509
+ for (let j = 0; j <= samples; j++) {
11510
+ for (let i = 0; i <= samples; i++)
11511
+ addSample((x + i / samples) * size, (y + j / samples) * size);
11512
+ }
11513
+ // Zoomed in, the visible part of the globe can be much smaller than the sample spacing.
11514
+ // So also sample the point of the tile closest to the map center.
11515
+ const x0 = x * size;
11516
+ const centerX = this.#centerX - Math.round(this.#centerX - (x0 + size / 2));
11517
+ addSample(clamp(centerX, x0, x0 + size), clamp(this.#centerY, y * size, (y + 1) * size));
11518
+ // A generous margin: the samples may miss the part of the tile at the silhouette.
11519
+ const margin = Math.max(maxX - minX, maxY - minY) / samples + 1;
11520
+ return (minX - margin <= this.width &&
11521
+ maxX + margin >= 0 &&
11522
+ minY - margin <= this.height &&
11523
+ maxY + margin >= 0);
11524
+ }
11525
+ }
11526
+ function clamp(value, min, max) {
11527
+ return Math.min(max, Math.max(min, value));
11528
+ }
11529
+
10479
11530
  function resolveTokens(text, properties) {
10480
11531
  return text.replace(/\{([^}]+)\}/g, (_, key) => {
10481
11532
  const value = properties[key];
@@ -10489,6 +11540,16 @@ function resolveTokens(text, properties) {
10489
11540
  });
10490
11541
  }
10491
11542
  async function renderMap(job) {
11543
+ job.projection ??= Projection.fromStyle({
11544
+ width: job.renderer.width,
11545
+ height: job.renderer.height,
11546
+ center: job.view.center,
11547
+ zoom: job.view.zoom,
11548
+ projection: job.style.projection,
11549
+ });
11550
+ const clipCircle = job.projection.clipCircle;
11551
+ if (clipCircle)
11552
+ job.renderer.setClipCircle?.(clipCircle);
10492
11553
  await render(job);
10493
11554
  return job.renderer.getString();
10494
11555
  }