@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.
@@ -497,10 +497,7 @@
497
497
  "type": "enum",
498
498
  "values": { "image": {} }
499
499
  },
500
- "url": {
501
- "required": true,
502
- "type": "string"
503
- },
500
+ "url": { "type": "string" },
504
501
  "coordinates": {
505
502
  "required": true,
506
503
  "type": "array",
@@ -634,19 +631,28 @@
634
631
  },
635
632
  "property-type": "data-constant"
636
633
  } },
637
- "layout_fill-extrusion": { "visibility": {
638
- "type": "enum",
639
- "values": {
640
- "visible": {},
641
- "none": {}
642
- },
643
- "default": "visible",
644
- "expression": {
645
- "interpolated": false,
646
- "parameters": ["global-state"]
634
+ "layout_fill-extrusion": {
635
+ "visibility": {
636
+ "type": "enum",
637
+ "values": {
638
+ "visible": {},
639
+ "none": {}
640
+ },
641
+ "default": "visible",
642
+ "expression": {
643
+ "interpolated": false,
644
+ "parameters": ["global-state"]
645
+ },
646
+ "property-type": "data-constant"
647
647
  },
648
- "property-type": "data-constant"
649
- } },
648
+ "fill-extrusion-rounded-corner-distance": {
649
+ "type": "number",
650
+ "default": 0,
651
+ "minimum": 0,
652
+ "units": "meters",
653
+ "property-type": "constant"
654
+ }
655
+ },
650
656
  layout_line: {
651
657
  "line-cap": {
652
658
  "type": "enum",
@@ -831,9 +837,9 @@
831
837
  "requires": ["icon-image"],
832
838
  "expression": {
833
839
  "interpolated": false,
834
- "parameters": ["zoom"]
840
+ "parameters": ["zoom", "feature"]
835
841
  },
836
- "property-type": "data-constant"
842
+ "property-type": "data-driven"
837
843
  },
838
844
  "icon-size": {
839
845
  "type": "number",
@@ -1294,6 +1300,31 @@
1294
1300
  },
1295
1301
  "property-type": "data-constant"
1296
1302
  },
1303
+ "symbol-height-offset": {
1304
+ "type": "number",
1305
+ "default": 0,
1306
+ "units": "meters",
1307
+ "requires": [{ "symbol-placement": ["point"] }],
1308
+ "expression": {
1309
+ "interpolated": true,
1310
+ "parameters": ["zoom", "feature"]
1311
+ },
1312
+ "property-type": "data-driven"
1313
+ },
1314
+ "symbol-height-anchor": {
1315
+ "type": "enum",
1316
+ "values": {
1317
+ "ground": {},
1318
+ "absolute": {}
1319
+ },
1320
+ "default": "ground",
1321
+ "requires": ["symbol-height-offset"],
1322
+ "expression": {
1323
+ "interpolated": false,
1324
+ "parameters": ["zoom"]
1325
+ },
1326
+ "property-type": "data-constant"
1327
+ },
1297
1328
  "visibility": {
1298
1329
  "type": "enum",
1299
1330
  "values": {
@@ -1769,7 +1800,6 @@
1769
1800
  "fill-extrusion-height": {
1770
1801
  "type": "number",
1771
1802
  "default": 0,
1772
- "minimum": 0,
1773
1803
  "units": "meters",
1774
1804
  "transition": true,
1775
1805
  "expression": {
@@ -1785,7 +1815,6 @@
1785
1815
  "fill-extrusion-base": {
1786
1816
  "type": "number",
1787
1817
  "default": 0,
1788
- "minimum": 0,
1789
1818
  "units": "meters",
1790
1819
  "transition": true,
1791
1820
  "requires": ["fill-extrusion-height"],
@@ -2820,44 +2849,10 @@
2820
2849
  }
2821
2850
  }
2822
2851
  };
2823
- //#endregion
2852
+
2824
2853
  //#region src/reference/latest.ts
2825
2854
  const latest = v8_default;
2826
- //#endregion
2827
- //#region src/expression/parsing_error.ts
2828
- var ExpressionParsingError = class extends Error {
2829
- constructor(key, message) {
2830
- super(message);
2831
- this.message = message;
2832
- this.key = key;
2833
- }
2834
- };
2835
- //#endregion
2836
- //#region src/expression/scope.ts
2837
- /**
2838
- * Tracks `let` bindings during expression parsing.
2839
- * @private
2840
- */
2841
- var Scope = class Scope {
2842
- constructor(parent, bindings = []) {
2843
- this.parent = parent;
2844
- this.bindings = {};
2845
- for (const [name, expression] of bindings) this.bindings[name] = expression;
2846
- }
2847
- concat(bindings) {
2848
- return new Scope(this, bindings);
2849
- }
2850
- get(name) {
2851
- if (this.bindings[name]) return this.bindings[name];
2852
- if (this.parent) return this.parent.get(name);
2853
- throw new Error(`${name} not found in scope.`);
2854
- }
2855
- has(name) {
2856
- if (this.bindings[name]) return true;
2857
- return this.parent ? this.parent.has(name) : false;
2858
- }
2859
- };
2860
- //#endregion
2855
+
2861
2856
  //#region src/expression/types.ts
2862
2857
  const NullType = { kind: "null" };
2863
2858
  const NumberType = { kind: "number" };
@@ -2953,7 +2948,7 @@
2953
2948
  if (provided.kind === "array" && sample.kind === "array") return provided.itemType.kind === sample.itemType.kind && typeof provided.N === "number";
2954
2949
  return provided.kind === sample.kind;
2955
2950
  }
2956
- //#endregion
2951
+
2957
2952
  //#region src/expression/types/color_spaces.ts
2958
2953
  const Xn = .96422;
2959
2954
  const Yn = 1;
@@ -3048,7 +3043,7 @@
3048
3043
  alpha
3049
3044
  ];
3050
3045
  }
3051
- //#endregion
3046
+
3052
3047
  //#region src/util/get_own.ts
3053
3048
  const hasOwnProperty = Object.hasOwn || function hasOwnProperty(object, key) {
3054
3049
  return Object.prototype.hasOwnProperty.call(object, key);
@@ -3056,7 +3051,7 @@
3056
3051
  function getOwn(object, key) {
3057
3052
  return hasOwnProperty(object, key) ? object[key] : void 0;
3058
3053
  }
3059
- //#endregion
3054
+
3060
3055
  //#region src/expression/types/parse_css_color.ts
3061
3056
  /**
3062
3057
  * CSS color parser compliant with CSS Color 4 Specification.
@@ -3135,9 +3130,9 @@
3135
3130
  const maxValue = valFormat === "%%%" ? 100 : valFormat === "" ? 255 : 0;
3136
3131
  if (maxValue) {
3137
3132
  const rgba = [
3138
- clamp(+r / maxValue, 0, 1),
3139
- clamp(+g / maxValue, 0, 1),
3140
- clamp(+b / maxValue, 0, 1),
3133
+ clamp$1(+r / maxValue, 0, 1),
3134
+ clamp$1(+g / maxValue, 0, 1),
3135
+ clamp$1(+b / maxValue, 0, 1),
3141
3136
  a ? parseAlpha(+a, ap) : 1
3142
3137
  ];
3143
3138
  if (validateNumbers(rgba)) return rgba;
@@ -3157,8 +3152,8 @@
3157
3152
  if (argFormat === " " || argFormat === " /" || argFormat === ",," || argFormat === ",,,") {
3158
3153
  const hsla = [
3159
3154
  +h,
3160
- clamp(+s, 0, 100),
3161
- clamp(+l, 0, 100),
3155
+ clamp$1(+s, 0, 100),
3156
+ clamp$1(+l, 0, 100),
3162
3157
  a ? parseAlpha(+a, ap) : 1
3163
3158
  ];
3164
3159
  if (validateNumbers(hsla)) return hslToRgb(hsla);
@@ -3169,9 +3164,9 @@
3169
3164
  return parseInt(hex.padEnd(2, hex), 16) / 255;
3170
3165
  }
3171
3166
  function parseAlpha(a, asPercentage) {
3172
- return clamp(asPercentage ? a / 100 : a, 0, 1);
3167
+ return clamp$1(asPercentage ? a / 100 : a, 0, 1);
3173
3168
  }
3174
- function clamp(n, min, max) {
3169
+ function clamp$1(n, min, max) {
3175
3170
  return Math.min(Math.max(min, n), max);
3176
3171
  }
3177
3172
  /**
@@ -3934,7 +3929,7 @@
3934
3929
  50
3935
3930
  ]
3936
3931
  };
3937
- //#endregion
3932
+
3938
3933
  //#region src/util/interpolate-primitives.ts
3939
3934
  function interpolateNumber(from, to, t) {
3940
3935
  return from + t * (to - from);
@@ -3944,7 +3939,7 @@
3944
3939
  return interpolateNumber(d, to[i], t);
3945
3940
  });
3946
3941
  }
3947
- //#endregion
3942
+
3948
3943
  //#region src/expression/types/color.ts
3949
3944
  /**
3950
3945
  * Checks whether the specified color space is one of the supported interpolation color spaces.
@@ -4128,26 +4123,7 @@
4128
4123
  }
4129
4124
  }
4130
4125
  };
4131
- //#endregion
4132
- //#region src/expression/types/collator.ts
4133
- var Collator = class {
4134
- constructor(caseSensitive, diacriticSensitive, locale) {
4135
- if (caseSensitive) this.sensitivity = diacriticSensitive ? "variant" : "case";
4136
- else this.sensitivity = diacriticSensitive ? "accent" : "base";
4137
- this.locale = locale;
4138
- this.collator = new Intl.Collator(this.locale ? this.locale : [], {
4139
- sensitivity: this.sensitivity,
4140
- usage: "search"
4141
- });
4142
- }
4143
- compare(lhs, rhs) {
4144
- return this.collator.compare(lhs, rhs);
4145
- }
4146
- resolvedLocale() {
4147
- return new Intl.Collator(this.locale ? this.locale : []).resolvedOptions().locale;
4148
- }
4149
- };
4150
- //#endregion
4126
+
4151
4127
  //#region src/expression/types/formatted.ts
4152
4128
  const VERTICAL_ALIGN_OPTIONS = [
4153
4129
  "bottom",
@@ -4184,7 +4160,7 @@
4184
4160
  return this.sections.map((section) => section.text).join("");
4185
4161
  }
4186
4162
  };
4187
- //#endregion
4163
+
4188
4164
  //#region src/expression/types/padding.ts
4189
4165
  /**
4190
4166
  * A set of four numbers representing padding around a box. Create instances from
@@ -4228,14 +4204,12 @@
4228
4204
  input[1]
4229
4205
  ];
4230
4206
  break;
4231
- case 3:
4232
- input = [
4233
- input[0],
4234
- input[1],
4235
- input[2],
4236
- input[1]
4237
- ];
4238
- break;
4207
+ case 3: input = [
4208
+ input[0],
4209
+ input[1],
4210
+ input[2],
4211
+ input[1]
4212
+ ];
4239
4213
  }
4240
4214
  return new Padding(input);
4241
4215
  }
@@ -4246,7 +4220,7 @@
4246
4220
  return new Padding(interpolateArray(from.values, to.values, t));
4247
4221
  }
4248
4222
  };
4249
- //#endregion
4223
+
4250
4224
  //#region src/expression/types/number_array.ts
4251
4225
  /**
4252
4226
  * An array of numbers. Create instances from
@@ -4276,7 +4250,7 @@
4276
4250
  return new NumberArray(interpolateArray(from.values, to.values, t));
4277
4251
  }
4278
4252
  };
4279
- //#endregion
4253
+
4280
4254
  //#region src/expression/types/color_array.ts
4281
4255
  /**
4282
4256
  * An array of colors. Create instances from
@@ -4319,7 +4293,7 @@
4319
4293
  return new ColorArray(colors);
4320
4294
  }
4321
4295
  };
4322
- //#endregion
4296
+
4323
4297
  //#region src/expression/runtime_error.ts
4324
4298
  var RuntimeError = class extends Error {
4325
4299
  constructor(message, path) {
@@ -4331,7 +4305,7 @@
4331
4305
  return this.message;
4332
4306
  }
4333
4307
  };
4334
- //#endregion
4308
+
4335
4309
  //#region src/expression/types/variable_anchor_offset_collection.ts
4336
4310
  /** Set of valid anchor positions, as a set for validation */
4337
4311
  const anchors = /* @__PURE__ */ new Set([
@@ -4383,7 +4357,7 @@
4383
4357
  return new VariableAnchorOffsetCollection(output);
4384
4358
  }
4385
4359
  };
4386
- //#endregion
4360
+
4387
4361
  //#region src/expression/types/resolved_image.ts
4388
4362
  var ResolvedImage = class ResolvedImage {
4389
4363
  constructor(options) {
@@ -4401,7 +4375,7 @@
4401
4375
  });
4402
4376
  }
4403
4377
  };
4404
- //#endregion
4378
+
4405
4379
  //#region src/expression/types/projection_definition.ts
4406
4380
  var ProjectionDefinition = class ProjectionDefinition {
4407
4381
  constructor(from, to, transition) {
@@ -4427,7 +4401,26 @@
4427
4401
  if (typeof input === "string") return new ProjectionDefinition(input, input, 1);
4428
4402
  }
4429
4403
  };
4430
- //#endregion
4404
+
4405
+ //#region src/expression/types/collator.ts
4406
+ var Collator = class {
4407
+ constructor(caseSensitive, diacriticSensitive, locale) {
4408
+ if (caseSensitive) this.sensitivity = diacriticSensitive ? "variant" : "case";
4409
+ else this.sensitivity = diacriticSensitive ? "accent" : "base";
4410
+ this.locale = locale;
4411
+ this.collator = new Intl.Collator(this.locale ? this.locale : [], {
4412
+ sensitivity: this.sensitivity,
4413
+ usage: "search"
4414
+ });
4415
+ }
4416
+ compare(lhs, rhs) {
4417
+ return this.collator.compare(lhs, rhs);
4418
+ }
4419
+ resolvedLocale() {
4420
+ return new Intl.Collator(this.locale ? this.locale : []).resolvedOptions().locale;
4421
+ }
4422
+ };
4423
+
4431
4424
  //#region src/expression/values.ts
4432
4425
  function validateRGBA(r, g, b, a) {
4433
4426
  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" ? [
@@ -4494,7 +4487,7 @@
4494
4487
  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();
4495
4488
  else return JSON.stringify(value);
4496
4489
  }
4497
- //#endregion
4490
+
4498
4491
  //#region src/expression/definitions/literal.ts
4499
4492
  var Literal = class Literal {
4500
4493
  constructor(type, value) {
@@ -4503,7 +4496,7 @@
4503
4496
  }
4504
4497
  static parse(args, context) {
4505
4498
  if (args.length !== 2) return context.error(`'literal' expression requires exactly one argument, but found ${args.length - 1} instead.`);
4506
- if (!isValue(args[1])) return context.error("invalid value");
4499
+ if (!isValue(args[1])) return context.error(`invalid value of type "${typeof args[1]}"`);
4507
4500
  const value = args[1];
4508
4501
  let type = typeOf(value);
4509
4502
  const expected = context.expectedType;
@@ -4518,85 +4511,524 @@
4518
4511
  return true;
4519
4512
  }
4520
4513
  };
4521
- //#endregion
4522
- //#region src/expression/definitions/assertion.ts
4523
- const types$1 = {
4524
- string: StringType,
4525
- number: NumberType,
4526
- boolean: BooleanType,
4527
- object: ObjectType
4528
- };
4529
- var Assertion = class Assertion {
4530
- constructor(type, args, key) {
4531
- this.type = type;
4532
- this.args = args;
4533
- this.key = key;
4514
+
4515
+ //#region src/expression/evaluation_context.ts
4516
+ const geometryTypes = [
4517
+ "Unknown",
4518
+ "Point",
4519
+ "LineString",
4520
+ "Polygon"
4521
+ ];
4522
+ var EvaluationContext = class {
4523
+ constructor() {
4524
+ this.globals = null;
4525
+ this.feature = null;
4526
+ this.featureState = null;
4527
+ this.formattedSection = null;
4528
+ this._parseColorCache = /* @__PURE__ */ new Map();
4529
+ this.availableImages = null;
4530
+ this.canonical = null;
4534
4531
  }
4535
- static parse(args, context) {
4536
- if (args.length < 2) return context.error("Expected at least one argument.");
4537
- let i = 1;
4538
- let type;
4539
- const name = args[0];
4540
- if (name === "array") {
4541
- let itemType;
4542
- if (args.length > 2) {
4543
- const type = args[1];
4544
- 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);
4545
- itemType = types$1[type];
4546
- i++;
4547
- } else itemType = ValueType;
4548
- let N;
4549
- if (args.length > 3) {
4550
- 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);
4551
- N = args[2];
4552
- i++;
4553
- }
4554
- type = array(itemType, N);
4555
- } else {
4556
- if (!types$1[name]) throw new Error(`Types doesn't contain name = ${name}`);
4557
- type = types$1[name];
4558
- }
4559
- const parsed = [];
4560
- for (; i < args.length; i++) {
4561
- const input = context.parse(args[i], i, ValueType);
4562
- if (!input) return null;
4563
- parsed.push(input);
4564
- }
4565
- return new Assertion(type, parsed, context.key);
4532
+ id() {
4533
+ return this.feature && "id" in this.feature ? this.feature.id : null;
4566
4534
  }
4567
- evaluate(ctx) {
4568
- for (let i = 0; i < this.args.length; i++) {
4569
- const value = this.args[i].evaluate(ctx);
4570
- if (!checkSubtype(this.type, typeOf(value))) return value;
4571
- 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);
4572
- }
4573
- throw new Error();
4535
+ geometryType() {
4536
+ return this.feature ? typeof this.feature.type === "number" ? geometryTypes[this.feature.type] : this.feature.type : null;
4574
4537
  }
4575
- eachChild(fn) {
4576
- this.args.forEach(fn);
4538
+ geometry() {
4539
+ return this.feature && "geometry" in this.feature ? this.feature.geometry : null;
4577
4540
  }
4578
- outputDefined() {
4579
- return this.args.every((arg) => arg.outputDefined());
4541
+ canonicalID() {
4542
+ return this.canonical;
4543
+ }
4544
+ properties() {
4545
+ return this.feature && this.feature.properties || {};
4546
+ }
4547
+ parseColor(input) {
4548
+ let cached = this._parseColorCache.get(input);
4549
+ if (!cached) {
4550
+ cached = Color$1.parse(input);
4551
+ this._parseColorCache.set(input, cached);
4552
+ }
4553
+ return cached;
4580
4554
  }
4581
4555
  };
4582
- //#endregion
4583
- //#region src/expression/definitions/coercion.ts
4584
- const types = {
4585
- "to-boolean": BooleanType,
4586
- "to-color": ColorType,
4587
- "to-number": NumberType,
4588
- "to-string": StringType
4589
- };
4556
+
4557
+ //#region src/expression/stops.ts
4590
4558
  /**
4591
- * Special form for error-coalescing coercion expressions "to-number",
4592
- * "to-color". Since these coercions can fail at runtime, they accept multiple
4593
- * arguments, only evaluating one at a time until one succeeds.
4594
- *
4559
+ * Returns the index of the last stop <= input, or 0 if it doesn't exist.
4595
4560
  * @private
4596
4561
  */
4597
- var Coercion = class Coercion {
4598
- constructor(type, args, key) {
4599
- this.type = type;
4562
+ function findStopLessThanOrEqualTo(stops, input, key) {
4563
+ const lastIndex = stops.length - 1;
4564
+ let lowerIndex = 0;
4565
+ let upperIndex = lastIndex;
4566
+ let currentIndex = 0;
4567
+ let currentValue, nextValue;
4568
+ while (lowerIndex <= upperIndex) {
4569
+ currentIndex = Math.floor((lowerIndex + upperIndex) / 2);
4570
+ currentValue = stops[currentIndex];
4571
+ nextValue = stops[currentIndex + 1];
4572
+ if (currentValue <= input) {
4573
+ if (currentIndex === lastIndex || input < nextValue) return currentIndex;
4574
+ lowerIndex = currentIndex + 1;
4575
+ } else if (currentValue > input) upperIndex = currentIndex - 1;
4576
+ else throw new RuntimeError("Input is not a number.", key);
4577
+ }
4578
+ return 0;
4579
+ }
4580
+
4581
+ //#region src/expression/definitions/step.ts
4582
+ var Step = class Step {
4583
+ constructor(type, input, stops, key) {
4584
+ this.type = type;
4585
+ this.input = input;
4586
+ this.key = key;
4587
+ this.labels = [];
4588
+ this.outputs = [];
4589
+ for (const [label, expression] of stops) {
4590
+ this.labels.push(label);
4591
+ this.outputs.push(expression);
4592
+ }
4593
+ }
4594
+ static parse(args, context) {
4595
+ if (args.length - 1 < 4) return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`);
4596
+ if ((args.length - 1) % 2 !== 0) return context.error("Expected an even number of arguments.");
4597
+ const input = context.parse(args[1], 1, NumberType);
4598
+ if (!input) return null;
4599
+ const stops = [];
4600
+ let outputType = null;
4601
+ if (context.expectedType && context.expectedType.kind !== "value") outputType = context.expectedType;
4602
+ for (let i = 1; i < args.length; i += 2) {
4603
+ const label = i === 1 ? -Infinity : args[i];
4604
+ const value = args[i + 1];
4605
+ const labelKey = i;
4606
+ const valueKey = i + 1;
4607
+ 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);
4608
+ 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);
4609
+ const parsed = context.parse(value, valueKey, outputType);
4610
+ if (!parsed) return null;
4611
+ outputType = outputType || parsed.type;
4612
+ stops.push([label, parsed]);
4613
+ }
4614
+ return new Step(outputType, input, stops, context.key);
4615
+ }
4616
+ evaluate(ctx) {
4617
+ const labels = this.labels;
4618
+ const outputs = this.outputs;
4619
+ if (labels.length === 1) return outputs[0].evaluate(ctx);
4620
+ const value = this.input.evaluate(ctx);
4621
+ if (value <= labels[0]) return outputs[0].evaluate(ctx);
4622
+ const stopCount = labels.length;
4623
+ if (value >= labels[stopCount - 1]) return outputs[stopCount - 1].evaluate(ctx);
4624
+ return outputs[findStopLessThanOrEqualTo(labels, value, this.key)].evaluate(ctx);
4625
+ }
4626
+ eachChild(fn) {
4627
+ fn(this.input);
4628
+ for (const expression of this.outputs) fn(expression);
4629
+ }
4630
+ outputDefined() {
4631
+ return this.outputs.every((out) => out.outputDefined());
4632
+ }
4633
+ };
4634
+
4635
+ //#region node_modules/@mapbox/unitbezier/index.js
4636
+ function unitBezier(p1x, p1y, p2x, p2y) {
4637
+ const cx = 3 * p1x;
4638
+ const bx = 3 * (p2x - p1x) - cx;
4639
+ const ax = 1 - cx - bx;
4640
+ const cy = 3 * p1y;
4641
+ const by = 3 * (p2y - p1y) - cy;
4642
+ const ay = 1 - cy - by;
4643
+ return function solve(x, epsilon = 1e-6) {
4644
+ if (x <= 0) return 0;
4645
+ if (x >= 1) return 1;
4646
+ let t = x;
4647
+ for (let i = 0; i < 8; i++) {
4648
+ const x2 = ((ax * t + bx) * t + cx) * t - x;
4649
+ if (Math.abs(x2) < epsilon) return ((ay * t + by) * t + cy) * t;
4650
+ const d2 = (3 * ax * t + 2 * bx) * t + cx;
4651
+ if (Math.abs(d2) < 1e-6) break;
4652
+ t -= x2 / d2;
4653
+ }
4654
+ let t0 = 0;
4655
+ let t1 = 1;
4656
+ t = x;
4657
+ for (let i = 0; i < 20; i++) {
4658
+ const x2 = ((ax * t + bx) * t + cx) * t;
4659
+ if (Math.abs(x2 - x) < epsilon) break;
4660
+ if (x > x2) t0 = t;
4661
+ else t1 = t;
4662
+ t = (t0 + t1) * .5;
4663
+ }
4664
+ return ((ay * t + by) * t + cy) * t;
4665
+ };
4666
+ }
4667
+
4668
+ //#region src/expression/definitions/interpolate.ts
4669
+ var Interpolate = class Interpolate {
4670
+ constructor(type, operator, interpolation, input, stops, key) {
4671
+ this.type = type;
4672
+ this.operator = operator;
4673
+ this.interpolation = interpolation;
4674
+ this.input = input;
4675
+ this.key = key;
4676
+ this.labels = [];
4677
+ this.outputs = [];
4678
+ for (const [label, expression] of stops) {
4679
+ this.labels.push(label);
4680
+ this.outputs.push(expression);
4681
+ }
4682
+ }
4683
+ static interpolationFactor(interpolation, input, lower, upper) {
4684
+ let t = 0;
4685
+ if (interpolation.name === "exponential") t = exponentialInterpolation(input, interpolation.base, lower, upper);
4686
+ else if (interpolation.name === "linear") t = exponentialInterpolation(input, 1, lower, upper);
4687
+ else if (interpolation.name === "cubic-bezier") {
4688
+ const c = interpolation.controlPoints;
4689
+ t = unitBezier(c[0], c[1], c[2], c[3])(exponentialInterpolation(input, 1, lower, upper));
4690
+ }
4691
+ return t;
4692
+ }
4693
+ static parse(args, context) {
4694
+ let [operator, interpolation, input, ...rest] = args;
4695
+ if (!Array.isArray(interpolation) || interpolation.length === 0) return context.error("Expected an interpolation type expression.", 1);
4696
+ if (interpolation[0] === "linear") interpolation = { name: "linear" };
4697
+ else if (interpolation[0] === "exponential") {
4698
+ const base = interpolation[1];
4699
+ if (typeof base !== "number") return context.error("Exponential interpolation requires a numeric base.", 1, 1);
4700
+ interpolation = {
4701
+ name: "exponential",
4702
+ base
4703
+ };
4704
+ } else if (interpolation[0] === "cubic-bezier") {
4705
+ const controlPoints = interpolation.slice(1);
4706
+ 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);
4707
+ interpolation = {
4708
+ name: "cubic-bezier",
4709
+ controlPoints
4710
+ };
4711
+ } else return context.error(`Unknown interpolation type ${String(interpolation[0])}`, 1, 0);
4712
+ if (args.length - 1 < 4) return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`);
4713
+ if ((args.length - 1) % 2 !== 0) return context.error("Expected an even number of arguments.");
4714
+ input = context.parse(input, 2, NumberType);
4715
+ if (!input) return null;
4716
+ const stops = [];
4717
+ let outputType = null;
4718
+ if ((operator === "interpolate-hcl" || operator === "interpolate-lab") && context.expectedType != ColorArrayType) outputType = ColorType;
4719
+ else if (context.expectedType && context.expectedType.kind !== "value") outputType = context.expectedType;
4720
+ for (let i = 0; i < rest.length; i += 2) {
4721
+ const label = rest[i];
4722
+ const value = rest[i + 1];
4723
+ const labelKey = i + 3;
4724
+ const valueKey = i + 4;
4725
+ 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);
4726
+ 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);
4727
+ const parsed = context.parse(value, valueKey, outputType);
4728
+ if (!parsed) return null;
4729
+ outputType = outputType || parsed.type;
4730
+ stops.push([label, parsed]);
4731
+ }
4732
+ 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.`);
4733
+ return new Interpolate(outputType, operator, interpolation, input, stops, context.key);
4734
+ }
4735
+ evaluate(ctx) {
4736
+ const labels = this.labels;
4737
+ const outputs = this.outputs;
4738
+ if (labels.length === 1) return outputs[0].evaluate(ctx);
4739
+ const value = this.input.evaluate(ctx);
4740
+ if (value <= labels[0]) return outputs[0].evaluate(ctx);
4741
+ const stopCount = labels.length;
4742
+ if (value >= labels[stopCount - 1]) return outputs[stopCount - 1].evaluate(ctx);
4743
+ const index = findStopLessThanOrEqualTo(labels, value, this.key);
4744
+ const lower = labels[index];
4745
+ const upper = labels[index + 1];
4746
+ const t = Interpolate.interpolationFactor(this.interpolation, value, lower, upper);
4747
+ const outputLower = outputs[index].evaluate(ctx);
4748
+ const outputUpper = outputs[index + 1].evaluate(ctx);
4749
+ switch (this.operator) {
4750
+ case "interpolate": switch (this.type.kind) {
4751
+ case "number": return interpolateNumber(outputLower, outputUpper, t);
4752
+ case "color": return Color$1.interpolate(outputLower, outputUpper, t);
4753
+ case "padding": return Padding.interpolate(outputLower, outputUpper, t);
4754
+ case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t);
4755
+ case "numberArray": return NumberArray.interpolate(outputLower, outputUpper, t);
4756
+ case "variableAnchorOffsetCollection": return VariableAnchorOffsetCollection.interpolate(outputLower, outputUpper, t, this.key);
4757
+ case "array": return interpolateArray(outputLower, outputUpper, t);
4758
+ case "projectionDefinition": return ProjectionDefinition.interpolate(outputLower, outputUpper, t);
4759
+ }
4760
+ case "interpolate-hcl": switch (this.type.kind) {
4761
+ case "color": return Color$1.interpolate(outputLower, outputUpper, t, "hcl");
4762
+ case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t, "hcl");
4763
+ }
4764
+ case "interpolate-lab": switch (this.type.kind) {
4765
+ case "color": return Color$1.interpolate(outputLower, outputUpper, t, "lab");
4766
+ case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t, "lab");
4767
+ }
4768
+ }
4769
+ }
4770
+ eachChild(fn) {
4771
+ fn(this.input);
4772
+ for (const expression of this.outputs) fn(expression);
4773
+ }
4774
+ outputDefined() {
4775
+ return this.outputs.every((out) => out.outputDefined());
4776
+ }
4777
+ };
4778
+ /**
4779
+ * Returns a ratio that can be used to interpolate between exponential function
4780
+ * stops.
4781
+ * 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,
4782
+ * and `a` and `b` are constants affording sufficient degrees of freedom to fit
4783
+ * the function to the given stops.
4784
+ *
4785
+ * Here's a bit of algebra that lets us compute `f(x)` directly from the stop
4786
+ * values without explicitly solving for `a` and `b`:
4787
+ *
4788
+ * First stop value: `f(x0) = y0 = a * base^x0 + b`
4789
+ * Second stop value: `f(x1) = y1 = a * base^x1 + b`
4790
+ * => `y1 - y0 = a(base^x1 - base^x0)`
4791
+ * => `a = (y1 - y0)/(base^x1 - base^x0)`
4792
+ *
4793
+ * Desired value: `f(x) = y = a * base^x + b`
4794
+ * => `f(x) = y0 + a * (base^x - base^x0)`
4795
+ *
4796
+ * From the above, we can replace the `a` in `a * (base^x - base^x0)` and do a
4797
+ * little algebra:
4798
+ * ```
4799
+ * a * (base^x - base^x0) = (y1 - y0)/(base^x1 - base^x0) * (base^x - base^x0)
4800
+ * = (y1 - y0) * (base^x - base^x0) / (base^x1 - base^x0)
4801
+ * ```
4802
+ *
4803
+ * If we let `(base^x - base^x0) / (base^x1 base^x0)`, then we have
4804
+ * `f(x) = y0 + (y1 - y0) * ratio`. In other words, `ratio` may be treated as
4805
+ * an interpolation factor between the two stops' output values.
4806
+ *
4807
+ * (Note: a slightly different form for `ratio`,
4808
+ * `(base^(x-x0) - 1) / (base^(x1-x0) - 1) `, is equivalent, but requires fewer
4809
+ * expensive `Math.pow()` operations.)
4810
+ *
4811
+ * @private
4812
+ */
4813
+ function exponentialInterpolation(input, base, lowerValue, upperValue) {
4814
+ const difference = upperValue - lowerValue;
4815
+ const progress = input - lowerValue;
4816
+ if (difference === 0) return 0;
4817
+ else if (base === 1) return progress / difference;
4818
+ else return (Math.pow(base, progress) - 1) / (Math.pow(base, difference) - 1);
4819
+ }
4820
+ const interpolateFactory = {
4821
+ color: Color$1.interpolate,
4822
+ number: interpolateNumber,
4823
+ padding: Padding.interpolate,
4824
+ numberArray: NumberArray.interpolate,
4825
+ colorArray: ColorArray.interpolate,
4826
+ variableAnchorOffsetCollection: VariableAnchorOffsetCollection.interpolate,
4827
+ array: interpolateArray
4828
+ };
4829
+
4830
+ //#region src/expression/definitions/format.ts
4831
+ var FormatExpression = class FormatExpression {
4832
+ constructor(sections) {
4833
+ this.type = FormattedType;
4834
+ this.sections = sections;
4835
+ }
4836
+ static parse(args, context) {
4837
+ if (args.length < 2) return context.error("Expected at least one argument.");
4838
+ const firstArg = args[1];
4839
+ if (!Array.isArray(firstArg) && typeof firstArg === "object") return context.error("First argument must be an image or text section.");
4840
+ const sections = [];
4841
+ let nextTokenMayBeObject = false;
4842
+ for (let i = 1; i <= args.length - 1; ++i) {
4843
+ const arg = args[i];
4844
+ if (nextTokenMayBeObject && typeof arg === "object" && !Array.isArray(arg)) {
4845
+ nextTokenMayBeObject = false;
4846
+ let scale = null;
4847
+ if (arg["font-scale"]) {
4848
+ scale = context.parse(arg["font-scale"], 1, NumberType);
4849
+ if (!scale) return null;
4850
+ }
4851
+ let font = null;
4852
+ if (arg["text-font"]) {
4853
+ font = context.parse(arg["text-font"], 1, array(StringType));
4854
+ if (!font) return null;
4855
+ }
4856
+ let textColor = null;
4857
+ if (arg["text-color"]) {
4858
+ textColor = context.parse(arg["text-color"], 1, ColorType);
4859
+ if (!textColor) return null;
4860
+ }
4861
+ let verticalAlign = null;
4862
+ if (arg["vertical-align"]) {
4863
+ 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.`);
4864
+ verticalAlign = context.parse(arg["vertical-align"], 1, StringType);
4865
+ if (!verticalAlign) return null;
4866
+ }
4867
+ const lastExpression = sections[sections.length - 1];
4868
+ lastExpression.scale = scale;
4869
+ lastExpression.font = font;
4870
+ lastExpression.textColor = textColor;
4871
+ lastExpression.verticalAlign = verticalAlign;
4872
+ } else {
4873
+ const content = context.parse(args[i], 1, ValueType);
4874
+ if (!content) return null;
4875
+ const kind = content.type.kind;
4876
+ if (kind !== "string" && kind !== "value" && kind !== "null" && kind !== "resolvedImage") return context.error("Formatted text type must be 'string', 'value', 'image' or 'null'.");
4877
+ nextTokenMayBeObject = true;
4878
+ sections.push({
4879
+ content,
4880
+ scale: null,
4881
+ font: null,
4882
+ textColor: null,
4883
+ verticalAlign: null
4884
+ });
4885
+ }
4886
+ }
4887
+ return new FormatExpression(sections);
4888
+ }
4889
+ evaluate(ctx) {
4890
+ const evaluateSection = (section) => {
4891
+ const evaluatedContent = section.content.evaluate(ctx);
4892
+ if (typeOf(evaluatedContent) === ResolvedImageType) return new FormattedSection("", evaluatedContent, null, null, null, section.verticalAlign ? section.verticalAlign.evaluate(ctx) : null);
4893
+ 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);
4894
+ };
4895
+ return new Formatted(this.sections.map(evaluateSection));
4896
+ }
4897
+ eachChild(fn) {
4898
+ for (const section of this.sections) {
4899
+ fn(section.content);
4900
+ if (section.scale) fn(section.scale);
4901
+ if (section.font) fn(section.font);
4902
+ if (section.textColor) fn(section.textColor);
4903
+ if (section.verticalAlign) fn(section.verticalAlign);
4904
+ }
4905
+ }
4906
+ outputDefined() {
4907
+ return false;
4908
+ }
4909
+ };
4910
+
4911
+ //#region src/util/classify_rings.ts
4912
+ /**
4913
+ * Classifies an array of rings into polygons with outer rings and holes
4914
+ * @param rings - the rings to classify
4915
+ * @param maxRings - the maximum number of rings to include in a polygon, use 0 to include all rings
4916
+ * @returns an array of polygons with internal rings as holes
4917
+ */
4918
+ function classifyRings$1(rings, maxRings) {
4919
+ if (rings.length <= 1) return [rings];
4920
+ const polygons = [];
4921
+ let polygon;
4922
+ let ccw;
4923
+ for (const ring of rings) {
4924
+ const area = calculateSignedArea(ring);
4925
+ if (area === 0) continue;
4926
+ ring.area = Math.abs(area);
4927
+ if (ccw === void 0) ccw = area < 0;
4928
+ if (ccw === area < 0) {
4929
+ if (polygon) polygons.push(polygon);
4930
+ polygon = [ring];
4931
+ } else polygon.push(ring);
4932
+ }
4933
+ if (polygon) polygons.push(polygon);
4934
+ return polygons;
4935
+ }
4936
+ /**
4937
+ * Returns the signed area for the polygon ring. Positive areas are exterior rings and
4938
+ * have a clockwise winding. Negative areas are interior rings and have a counter clockwise
4939
+ * ordering.
4940
+ *
4941
+ * @param ring - Exterior or interior ring
4942
+ * @returns Signed area
4943
+ */
4944
+ function calculateSignedArea(ring) {
4945
+ let sum = 0;
4946
+ for (let i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) {
4947
+ p1 = ring[i];
4948
+ p2 = ring[j];
4949
+ sum += (p2.x - p1.x) * (p1.y + p2.y);
4950
+ }
4951
+ return sum;
4952
+ }
4953
+
4954
+ //#region src/expression/definitions/assertion.ts
4955
+ const types$1 = {
4956
+ string: StringType,
4957
+ number: NumberType,
4958
+ boolean: BooleanType,
4959
+ object: ObjectType
4960
+ };
4961
+ var Assertion = class Assertion {
4962
+ constructor(type, args, key) {
4963
+ this.type = type;
4964
+ this.args = args;
4965
+ this.key = key;
4966
+ }
4967
+ static parse(args, context) {
4968
+ if (args.length < 2) return context.error("Expected at least one argument.");
4969
+ let i = 1;
4970
+ let type;
4971
+ const name = args[0];
4972
+ if (name === "array") {
4973
+ let itemType;
4974
+ if (args.length > 2) {
4975
+ const type = args[1];
4976
+ 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);
4977
+ itemType = types$1[type];
4978
+ i++;
4979
+ } else itemType = ValueType;
4980
+ let N;
4981
+ if (args.length > 3) {
4982
+ 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);
4983
+ N = args[2];
4984
+ i++;
4985
+ }
4986
+ type = array(itemType, N);
4987
+ } else {
4988
+ if (!types$1[name]) throw new Error(`Types doesn't contain name = ${name}`);
4989
+ type = types$1[name];
4990
+ }
4991
+ const parsed = [];
4992
+ for (; i < args.length; i++) {
4993
+ const input = context.parse(args[i], i, ValueType);
4994
+ if (!input) return null;
4995
+ parsed.push(input);
4996
+ }
4997
+ return new Assertion(type, parsed, context.key);
4998
+ }
4999
+ evaluate(ctx) {
5000
+ for (let i = 0; i < this.args.length; i++) {
5001
+ const value = this.args[i].evaluate(ctx);
5002
+ if (!checkSubtype(this.type, typeOf(value))) return value;
5003
+ 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);
5004
+ }
5005
+ throw new Error();
5006
+ }
5007
+ eachChild(fn) {
5008
+ this.args.forEach(fn);
5009
+ }
5010
+ outputDefined() {
5011
+ return this.args.every((arg) => arg.outputDefined());
5012
+ }
5013
+ };
5014
+
5015
+ //#region src/expression/definitions/coercion.ts
5016
+ const types = {
5017
+ "to-boolean": BooleanType,
5018
+ "to-color": ColorType,
5019
+ "to-number": NumberType,
5020
+ "to-string": StringType
5021
+ };
5022
+ /**
5023
+ * Special form for error-coalescing coercion expressions "to-number",
5024
+ * "to-color". Since these coercions can fail at runtime, they accept multiple
5025
+ * arguments, only evaluating one at a time until one succeeds.
5026
+ *
5027
+ * @private
5028
+ */
5029
+ var Coercion = class Coercion {
5030
+ constructor(type, args, key) {
5031
+ this.type = type;
4600
5032
  this.args = args;
4601
5033
  this.key = key;
4602
5034
  }
@@ -4659,203 +5091,47 @@
4659
5091
  input = arg.evaluate(ctx);
4660
5092
  const val = ColorArray.parse(input);
4661
5093
  if (val) return val;
4662
- }
4663
- throw new RuntimeError(`Could not parse colorArray from value '${typeof input === "string" ? input : JSON.stringify(input)}'`, this.key);
4664
- }
4665
- case "variableAnchorOffsetCollection": {
4666
- let input;
4667
- for (const arg of this.args) {
4668
- input = arg.evaluate(ctx);
4669
- const coll = VariableAnchorOffsetCollection.parse(input);
4670
- if (coll) return coll;
4671
- }
4672
- throw new RuntimeError(`Could not parse variableAnchorOffsetCollection from value '${typeof input === "string" ? input : JSON.stringify(input)}'`, this.key);
4673
- }
4674
- case "number": {
4675
- let value = null;
4676
- for (const arg of this.args) {
4677
- value = arg.evaluate(ctx);
4678
- if (value === null) return 0;
4679
- const num = Number(value);
4680
- if (isNaN(num)) continue;
4681
- return num;
4682
- }
4683
- throw new RuntimeError(`Could not convert ${JSON.stringify(value)} to number.`, this.key);
4684
- }
4685
- case "formatted": return Formatted.fromString(valueToString(this.args[0].evaluate(ctx)));
4686
- case "resolvedImage": return ResolvedImage.fromString(valueToString(this.args[0].evaluate(ctx)));
4687
- case "projectionDefinition": return this.args[0].evaluate(ctx);
4688
- default: return valueToString(this.args[0].evaluate(ctx));
4689
- }
4690
- }
4691
- eachChild(fn) {
4692
- this.args.forEach(fn);
4693
- }
4694
- outputDefined() {
4695
- return this.args.every((arg) => arg.outputDefined());
4696
- }
4697
- };
4698
- //#endregion
4699
- //#region src/expression/evaluation_context.ts
4700
- const geometryTypes = [
4701
- "Unknown",
4702
- "Point",
4703
- "LineString",
4704
- "Polygon"
4705
- ];
4706
- var EvaluationContext = class {
4707
- constructor() {
4708
- this.globals = null;
4709
- this.feature = null;
4710
- this.featureState = null;
4711
- this.formattedSection = null;
4712
- this._parseColorCache = /* @__PURE__ */ new Map();
4713
- this.availableImages = null;
4714
- this.canonical = null;
4715
- }
4716
- id() {
4717
- return this.feature && "id" in this.feature ? this.feature.id : null;
4718
- }
4719
- geometryType() {
4720
- return this.feature ? typeof this.feature.type === "number" ? geometryTypes[this.feature.type] : this.feature.type : null;
4721
- }
4722
- geometry() {
4723
- return this.feature && "geometry" in this.feature ? this.feature.geometry : null;
4724
- }
4725
- canonicalID() {
4726
- return this.canonical;
4727
- }
4728
- properties() {
4729
- return this.feature && this.feature.properties || {};
4730
- }
4731
- parseColor(input) {
4732
- let cached = this._parseColorCache.get(input);
4733
- if (!cached) {
4734
- cached = Color$1.parse(input);
4735
- this._parseColorCache.set(input, cached);
4736
- }
4737
- return cached;
4738
- }
4739
- };
4740
- //#endregion
4741
- //#region src/expression/parsing_context.ts
4742
- /**
4743
- * State associated parsing at a given point in an expression tree.
4744
- * @private
4745
- */
4746
- var ParsingContext = class ParsingContext {
4747
- constructor(registry, isConstantFunc, path = [], expectedType, scope = new Scope(), errors = []) {
4748
- this.registry = registry;
4749
- this.path = path;
4750
- this.key = path.map((part) => `[${part}]`).join("");
4751
- this.scope = scope;
4752
- this.errors = errors;
4753
- this.expectedType = expectedType;
4754
- this._isConstant = isConstantFunc;
4755
- }
4756
- /**
4757
- * @param expr the JSON expression to parse
4758
- * @param index the optional argument index if this expression is an argument of a parent expression that's being parsed
4759
- * @param options
4760
- * @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.
4761
- * @private
4762
- */
4763
- parse(expr, index, expectedType, bindings, options = {}) {
4764
- if (index) return this.concat(index, expectedType, bindings)._parse(expr, options);
4765
- return this._parse(expr, options);
4766
- }
4767
- _parse(expr, options) {
4768
- if (expr === null || typeof expr === "string" || typeof expr === "boolean" || typeof expr === "number") expr = ["literal", expr];
4769
- const key = this.key;
4770
- function annotate(parsed, type, typeAnnotation) {
4771
- if (typeAnnotation === "assert") return new Assertion(type, [parsed], key);
4772
- else if (typeAnnotation === "coerce") return new Coercion(type, [parsed], key);
4773
- else return parsed;
4774
- }
4775
- if (Array.isArray(expr)) {
4776
- if (expr.length === 0) return this.error("Expected an array with at least one element. If you wanted a literal array, use [\"literal\", []].");
4777
- const op = expr[0];
4778
- if (typeof op !== "string") {
4779
- this.error(`Expression name must be a string, but found ${typeof op} instead. If you wanted a literal array, use ["literal", [...]].`, 0);
4780
- return null;
5094
+ }
5095
+ throw new RuntimeError(`Could not parse colorArray from value '${typeof input === "string" ? input : JSON.stringify(input)}'`, this.key);
4781
5096
  }
4782
- const Expr = this.registry[op];
4783
- if (Expr) {
4784
- let parsed = Expr.parse(expr, this);
4785
- if (!parsed) return null;
4786
- if (this.expectedType) {
4787
- const expected = this.expectedType;
4788
- const actual = parsed.type;
4789
- 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");
4790
- else if ("projectionDefinition" === expected.kind && ["string", "array"].includes(actual.kind) || [
4791
- "color",
4792
- "formatted",
4793
- "resolvedImage"
4794
- ].includes(expected.kind) && ["value", "string"].includes(actual.kind) || ["padding", "numberArray"].includes(expected.kind) && [
4795
- "value",
4796
- "number",
4797
- "array"
4798
- ].includes(actual.kind) || "colorArray" === expected.kind && [
4799
- "value",
4800
- "string",
4801
- "array"
4802
- ].includes(actual.kind) || "variableAnchorOffsetCollection" === expected.kind && ["value", "array"].includes(actual.kind)) parsed = annotate(parsed, expected, options.typeAnnotation || "coerce");
4803
- else if (this.checkSubtype(expected, actual)) return null;
5097
+ case "variableAnchorOffsetCollection": {
5098
+ let input;
5099
+ for (const arg of this.args) {
5100
+ input = arg.evaluate(ctx);
5101
+ const coll = VariableAnchorOffsetCollection.parse(input);
5102
+ if (coll) return coll;
4804
5103
  }
4805
- if (!(parsed instanceof Literal) && parsed.type.kind !== "resolvedImage" && this._isConstant(parsed)) {
4806
- const ec = new EvaluationContext();
4807
- try {
4808
- parsed = new Literal(parsed.type, parsed.evaluate(ec));
4809
- } catch (e) {
4810
- this.error(e.message);
4811
- return null;
4812
- }
5104
+ throw new RuntimeError(`Could not parse variableAnchorOffsetCollection from value '${typeof input === "string" ? input : JSON.stringify(input)}'`, this.key);
5105
+ }
5106
+ case "number": {
5107
+ let value = null;
5108
+ for (const arg of this.args) {
5109
+ value = arg.evaluate(ctx);
5110
+ if (value === null) return 0;
5111
+ const num = Number(value);
5112
+ if (isNaN(num)) continue;
5113
+ return num;
4813
5114
  }
4814
- return parsed;
5115
+ throw new RuntimeError(`Could not convert ${JSON.stringify(value)} to number.`, this.key);
4815
5116
  }
4816
- return this.error(`Unknown expression "${op}". If you wanted a literal array, use ["literal", [...]].`, 0);
4817
- } else if (typeof expr === "undefined") return this.error("'undefined' value invalid. Use null instead.");
4818
- else if (typeof expr === "object") return this.error("Bare objects invalid. Use [\"literal\", {...}] instead.");
4819
- else return this.error(`Expected an array, but found ${typeof expr} instead.`);
4820
- }
4821
- /**
4822
- * Returns a copy of this context suitable for parsing the subexpression at
4823
- * index `index`, optionally appending to 'let' binding map.
4824
- *
4825
- * Note that `errors` property, intended for collecting errors while
4826
- * parsing, is copied by reference rather than cloned.
4827
- * @private
4828
- */
4829
- concat(index, expectedType, bindings) {
4830
- const path = typeof index === "number" ? this.path.concat(index) : this.path;
4831
- const scope = bindings ? this.scope.concat(bindings) : this.scope;
4832
- return new ParsingContext(this.registry, this._isConstant, path, expectedType || null, scope, this.errors);
5117
+ case "formatted": return Formatted.fromString(valueToString(this.args[0].evaluate(ctx)));
5118
+ case "resolvedImage": return ResolvedImage.fromString(valueToString(this.args[0].evaluate(ctx)));
5119
+ case "projectionDefinition": {
5120
+ const input = this.args[0].evaluate(ctx);
5121
+ if (ProjectionDefinition.parse(input)) return input;
5122
+ throw new RuntimeError(`Could not parse projectionDefinition from value '${typeof input === "string" ? input : JSON.stringify(input)}'`, this.key);
5123
+ }
5124
+ default: return valueToString(this.args[0].evaluate(ctx));
5125
+ }
4833
5126
  }
4834
- /**
4835
- * Push a parsing (or type checking) error into the `this.errors`
4836
- * @param error The message
4837
- * @param keys Optionally specify the source of the error at a child
4838
- * of the current expression at `this.key`.
4839
- * @private
4840
- */
4841
- error(error, ...keys) {
4842
- const key = `${this.key}${keys.map((k) => `[${k}]`).join("")}`;
4843
- this.errors.push(new ExpressionParsingError(key, error));
5127
+ eachChild(fn) {
5128
+ this.args.forEach(fn);
4844
5129
  }
4845
- /**
4846
- * Returns null if `t` is a subtype of `expected`; otherwise returns an
4847
- * error message and also pushes it to `this.errors`.
4848
- * @param expected The expected type
4849
- * @param t The actual type
4850
- * @returns null if `t` is a subtype of `expected`; otherwise returns an error message
4851
- */
4852
- checkSubtype(expected, t) {
4853
- const error = checkSubtype(expected, t);
4854
- if (error) this.error(error);
4855
- return error;
5130
+ outputDefined() {
5131
+ return this.args.every((arg) => arg.outputDefined());
4856
5132
  }
4857
5133
  };
4858
- //#endregion
5134
+
4859
5135
  //#region src/expression/definitions/let.ts
4860
5136
  var Let = class Let {
4861
5137
  constructor(bindings, result) {
@@ -4889,7 +5165,7 @@
4889
5165
  return this.result.outputDefined();
4890
5166
  }
4891
5167
  };
4892
- //#endregion
5168
+
4893
5169
  //#region src/expression/definitions/var.ts
4894
5170
  var Var = class Var {
4895
5171
  constructor(name, boundExpression) {
@@ -4911,7 +5187,7 @@
4911
5187
  return false;
4912
5188
  }
4913
5189
  };
4914
- //#endregion
5190
+
4915
5191
  //#region src/expression/definitions/at.ts
4916
5192
  var At = class At {
4917
5193
  constructor(type, index, input, key) {
@@ -4944,7 +5220,7 @@
4944
5220
  return false;
4945
5221
  }
4946
5222
  };
4947
- //#endregion
5223
+
4948
5224
  //#region src/expression/definitions/in.ts
4949
5225
  var In = class In {
4950
5226
  constructor(needle, haystack, key) {
@@ -4988,7 +5264,7 @@
4988
5264
  return true;
4989
5265
  }
4990
5266
  };
4991
- //#endregion
5267
+
4992
5268
  //#region src/expression/definitions/index_of.ts
4993
5269
  var IndexOf = class IndexOf {
4994
5270
  constructor(needle, haystack, key, fromIndex) {
@@ -5043,7 +5319,7 @@
5043
5319
  return false;
5044
5320
  }
5045
5321
  };
5046
- //#endregion
5322
+
5047
5323
  //#region src/expression/definitions/match.ts
5048
5324
  var Match = class Match {
5049
5325
  constructor(inputType, outputType, input, cases, outputs, otherwise) {
@@ -5102,7 +5378,7 @@
5102
5378
  return this.outputs.every((out) => out.outputDefined()) && this.otherwise.outputDefined();
5103
5379
  }
5104
5380
  };
5105
- //#endregion
5381
+
5106
5382
  //#region src/expression/definitions/case.ts
5107
5383
  var Case = class Case {
5108
5384
  constructor(type, branches, otherwise) {
@@ -5144,324 +5420,51 @@
5144
5420
  return this.branches.every(([_, out]) => out.outputDefined()) && this.otherwise.outputDefined();
5145
5421
  }
5146
5422
  };
5147
- //#endregion
5423
+
5148
5424
  //#region src/expression/definitions/slice.ts
5149
5425
  var Slice = class Slice {
5150
- constructor(type, input, beginIndex, key, endIndex) {
5151
- this.type = type;
5152
- this.input = input;
5153
- this.beginIndex = beginIndex;
5154
- this.key = key;
5155
- this.endIndex = endIndex;
5156
- }
5157
- static parse(args, context) {
5158
- if (args.length <= 2 || args.length >= 5) return context.error(`Expected 2 or 3 arguments, but found ${args.length - 1} instead.`);
5159
- const input = context.parse(args[1], 1, ValueType);
5160
- const beginIndex = context.parse(args[2], 2, NumberType);
5161
- if (!input || !beginIndex) return null;
5162
- if (!isValidType(input.type, [
5163
- array(ValueType),
5164
- StringType,
5165
- ValueType
5166
- ])) return context.error(`Expected first argument to be of type array or string, but found ${typeToString(input.type)} instead`);
5167
- if (args.length === 4) {
5168
- const endIndex = context.parse(args[3], 3, NumberType);
5169
- if (!endIndex) return null;
5170
- return new Slice(input.type, input, beginIndex, context.key, endIndex);
5171
- } else return new Slice(input.type, input, beginIndex, context.key);
5172
- }
5173
- evaluate(ctx) {
5174
- const input = this.input.evaluate(ctx);
5175
- const beginIndex = this.beginIndex.evaluate(ctx);
5176
- let endIndex;
5177
- if (this.endIndex) endIndex = this.endIndex.evaluate(ctx);
5178
- if (isValidNativeType(input, ["string"])) return [...input].slice(beginIndex, endIndex).join("");
5179
- else if (isValidNativeType(input, ["array"])) return input.slice(beginIndex, endIndex);
5180
- else throw new RuntimeError(`Expected first argument to be of type array or string, but found ${typeToString(typeOf(input))} instead.`, this.key);
5181
- }
5182
- eachChild(fn) {
5183
- fn(this.input);
5184
- fn(this.beginIndex);
5185
- if (this.endIndex) fn(this.endIndex);
5186
- }
5187
- outputDefined() {
5188
- return false;
5189
- }
5190
- };
5191
- //#endregion
5192
- //#region src/expression/stops.ts
5193
- /**
5194
- * Returns the index of the last stop <= input, or 0 if it doesn't exist.
5195
- * @private
5196
- */
5197
- function findStopLessThanOrEqualTo(stops, input, key) {
5198
- const lastIndex = stops.length - 1;
5199
- let lowerIndex = 0;
5200
- let upperIndex = lastIndex;
5201
- let currentIndex = 0;
5202
- let currentValue, nextValue;
5203
- while (lowerIndex <= upperIndex) {
5204
- currentIndex = Math.floor((lowerIndex + upperIndex) / 2);
5205
- currentValue = stops[currentIndex];
5206
- nextValue = stops[currentIndex + 1];
5207
- if (currentValue <= input) {
5208
- if (currentIndex === lastIndex || input < nextValue) return currentIndex;
5209
- lowerIndex = currentIndex + 1;
5210
- } else if (currentValue > input) upperIndex = currentIndex - 1;
5211
- else throw new RuntimeError("Input is not a number.", key);
5212
- }
5213
- return 0;
5214
- }
5215
- //#endregion
5216
- //#region src/expression/definitions/step.ts
5217
- var Step = class Step {
5218
- constructor(type, input, stops, key) {
5219
- this.type = type;
5220
- this.input = input;
5221
- this.key = key;
5222
- this.labels = [];
5223
- this.outputs = [];
5224
- for (const [label, expression] of stops) {
5225
- this.labels.push(label);
5226
- this.outputs.push(expression);
5227
- }
5228
- }
5229
- static parse(args, context) {
5230
- if (args.length - 1 < 4) return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`);
5231
- if ((args.length - 1) % 2 !== 0) return context.error("Expected an even number of arguments.");
5232
- const input = context.parse(args[1], 1, NumberType);
5233
- if (!input) return null;
5234
- const stops = [];
5235
- let outputType = null;
5236
- if (context.expectedType && context.expectedType.kind !== "value") outputType = context.expectedType;
5237
- for (let i = 1; i < args.length; i += 2) {
5238
- const label = i === 1 ? -Infinity : args[i];
5239
- const value = args[i + 1];
5240
- const labelKey = i;
5241
- const valueKey = i + 1;
5242
- 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);
5243
- 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);
5244
- const parsed = context.parse(value, valueKey, outputType);
5245
- if (!parsed) return null;
5246
- outputType = outputType || parsed.type;
5247
- stops.push([label, parsed]);
5248
- }
5249
- return new Step(outputType, input, stops, context.key);
5250
- }
5251
- evaluate(ctx) {
5252
- const labels = this.labels;
5253
- const outputs = this.outputs;
5254
- if (labels.length === 1) return outputs[0].evaluate(ctx);
5255
- const value = this.input.evaluate(ctx);
5256
- if (value <= labels[0]) return outputs[0].evaluate(ctx);
5257
- const stopCount = labels.length;
5258
- if (value >= labels[stopCount - 1]) return outputs[stopCount - 1].evaluate(ctx);
5259
- return outputs[findStopLessThanOrEqualTo(labels, value, this.key)].evaluate(ctx);
5260
- }
5261
- eachChild(fn) {
5262
- fn(this.input);
5263
- for (const expression of this.outputs) fn(expression);
5264
- }
5265
- outputDefined() {
5266
- return this.outputs.every((out) => out.outputDefined());
5267
- }
5268
- };
5269
- //#endregion
5270
- //#region node_modules/@mapbox/unitbezier/index.js
5271
- function unitBezier(p1x, p1y, p2x, p2y) {
5272
- const cx = 3 * p1x;
5273
- const bx = 3 * (p2x - p1x) - cx;
5274
- const ax = 1 - cx - bx;
5275
- const cy = 3 * p1y;
5276
- const by = 3 * (p2y - p1y) - cy;
5277
- const ay = 1 - cy - by;
5278
- return function solve(x, epsilon = 1e-6) {
5279
- if (x <= 0) return 0;
5280
- if (x >= 1) return 1;
5281
- let t = x;
5282
- for (let i = 0; i < 8; i++) {
5283
- const x2 = ((ax * t + bx) * t + cx) * t - x;
5284
- if (Math.abs(x2) < epsilon) return ((ay * t + by) * t + cy) * t;
5285
- const d2 = (3 * ax * t + 2 * bx) * t + cx;
5286
- if (Math.abs(d2) < 1e-6) break;
5287
- t -= x2 / d2;
5288
- }
5289
- let t0 = 0;
5290
- let t1 = 1;
5291
- t = x;
5292
- for (let i = 0; i < 20; i++) {
5293
- const x2 = ((ax * t + bx) * t + cx) * t;
5294
- if (Math.abs(x2 - x) < epsilon) break;
5295
- if (x > x2) t0 = t;
5296
- else t1 = t;
5297
- t = (t0 + t1) * .5;
5298
- }
5299
- return ((ay * t + by) * t + cy) * t;
5300
- };
5301
- }
5302
- //#endregion
5303
- //#region src/expression/definitions/interpolate.ts
5304
- var Interpolate = class Interpolate {
5305
- constructor(type, operator, interpolation, input, stops, key) {
5426
+ constructor(type, input, beginIndex, key, endIndex) {
5306
5427
  this.type = type;
5307
- this.operator = operator;
5308
- this.interpolation = interpolation;
5309
5428
  this.input = input;
5429
+ this.beginIndex = beginIndex;
5310
5430
  this.key = key;
5311
- this.labels = [];
5312
- this.outputs = [];
5313
- for (const [label, expression] of stops) {
5314
- this.labels.push(label);
5315
- this.outputs.push(expression);
5316
- }
5317
- }
5318
- static interpolationFactor(interpolation, input, lower, upper) {
5319
- let t = 0;
5320
- if (interpolation.name === "exponential") t = exponentialInterpolation(input, interpolation.base, lower, upper);
5321
- else if (interpolation.name === "linear") t = exponentialInterpolation(input, 1, lower, upper);
5322
- else if (interpolation.name === "cubic-bezier") {
5323
- const c = interpolation.controlPoints;
5324
- t = unitBezier(c[0], c[1], c[2], c[3])(exponentialInterpolation(input, 1, lower, upper));
5325
- }
5326
- return t;
5431
+ this.endIndex = endIndex;
5327
5432
  }
5328
5433
  static parse(args, context) {
5329
- let [operator, interpolation, input, ...rest] = args;
5330
- if (!Array.isArray(interpolation) || interpolation.length === 0) return context.error("Expected an interpolation type expression.", 1);
5331
- if (interpolation[0] === "linear") interpolation = { name: "linear" };
5332
- else if (interpolation[0] === "exponential") {
5333
- const base = interpolation[1];
5334
- if (typeof base !== "number") return context.error("Exponential interpolation requires a numeric base.", 1, 1);
5335
- interpolation = {
5336
- name: "exponential",
5337
- base
5338
- };
5339
- } else if (interpolation[0] === "cubic-bezier") {
5340
- const controlPoints = interpolation.slice(1);
5341
- 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);
5342
- interpolation = {
5343
- name: "cubic-bezier",
5344
- controlPoints
5345
- };
5346
- } else return context.error(`Unknown interpolation type ${String(interpolation[0])}`, 1, 0);
5347
- if (args.length - 1 < 4) return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`);
5348
- if ((args.length - 1) % 2 !== 0) return context.error("Expected an even number of arguments.");
5349
- input = context.parse(input, 2, NumberType);
5350
- if (!input) return null;
5351
- const stops = [];
5352
- let outputType = null;
5353
- if ((operator === "interpolate-hcl" || operator === "interpolate-lab") && context.expectedType != ColorArrayType) outputType = ColorType;
5354
- else if (context.expectedType && context.expectedType.kind !== "value") outputType = context.expectedType;
5355
- for (let i = 0; i < rest.length; i += 2) {
5356
- const label = rest[i];
5357
- const value = rest[i + 1];
5358
- const labelKey = i + 3;
5359
- const valueKey = i + 4;
5360
- 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);
5361
- 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);
5362
- const parsed = context.parse(value, valueKey, outputType);
5363
- if (!parsed) return null;
5364
- outputType = outputType || parsed.type;
5365
- stops.push([label, parsed]);
5366
- }
5367
- 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.`);
5368
- return new Interpolate(outputType, operator, interpolation, input, stops, context.key);
5434
+ if (args.length <= 2 || args.length >= 5) return context.error(`Expected 2 or 3 arguments, but found ${args.length - 1} instead.`);
5435
+ const input = context.parse(args[1], 1, ValueType);
5436
+ const beginIndex = context.parse(args[2], 2, NumberType);
5437
+ if (!input || !beginIndex) return null;
5438
+ if (!isValidType(input.type, [
5439
+ array(ValueType),
5440
+ StringType,
5441
+ ValueType
5442
+ ])) return context.error(`Expected first argument to be of type array or string, but found ${typeToString(input.type)} instead`);
5443
+ if (args.length === 4) {
5444
+ const endIndex = context.parse(args[3], 3, NumberType);
5445
+ if (!endIndex) return null;
5446
+ return new Slice(input.type, input, beginIndex, context.key, endIndex);
5447
+ } else return new Slice(input.type, input, beginIndex, context.key);
5369
5448
  }
5370
5449
  evaluate(ctx) {
5371
- const labels = this.labels;
5372
- const outputs = this.outputs;
5373
- if (labels.length === 1) return outputs[0].evaluate(ctx);
5374
- const value = this.input.evaluate(ctx);
5375
- if (value <= labels[0]) return outputs[0].evaluate(ctx);
5376
- const stopCount = labels.length;
5377
- if (value >= labels[stopCount - 1]) return outputs[stopCount - 1].evaluate(ctx);
5378
- const index = findStopLessThanOrEqualTo(labels, value, this.key);
5379
- const lower = labels[index];
5380
- const upper = labels[index + 1];
5381
- const t = Interpolate.interpolationFactor(this.interpolation, value, lower, upper);
5382
- const outputLower = outputs[index].evaluate(ctx);
5383
- const outputUpper = outputs[index + 1].evaluate(ctx);
5384
- switch (this.operator) {
5385
- case "interpolate": switch (this.type.kind) {
5386
- case "number": return interpolateNumber(outputLower, outputUpper, t);
5387
- case "color": return Color$1.interpolate(outputLower, outputUpper, t);
5388
- case "padding": return Padding.interpolate(outputLower, outputUpper, t);
5389
- case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t);
5390
- case "numberArray": return NumberArray.interpolate(outputLower, outputUpper, t);
5391
- case "variableAnchorOffsetCollection": return VariableAnchorOffsetCollection.interpolate(outputLower, outputUpper, t, this.key);
5392
- case "array": return interpolateArray(outputLower, outputUpper, t);
5393
- case "projectionDefinition": return ProjectionDefinition.interpolate(outputLower, outputUpper, t);
5394
- }
5395
- case "interpolate-hcl": switch (this.type.kind) {
5396
- case "color": return Color$1.interpolate(outputLower, outputUpper, t, "hcl");
5397
- case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t, "hcl");
5398
- }
5399
- case "interpolate-lab": switch (this.type.kind) {
5400
- case "color": return Color$1.interpolate(outputLower, outputUpper, t, "lab");
5401
- case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t, "lab");
5402
- }
5403
- }
5450
+ const input = this.input.evaluate(ctx);
5451
+ const beginIndex = this.beginIndex.evaluate(ctx);
5452
+ let endIndex;
5453
+ if (this.endIndex) endIndex = this.endIndex.evaluate(ctx);
5454
+ if (isValidNativeType(input, ["string"])) return [...input].slice(beginIndex, endIndex).join("");
5455
+ else if (isValidNativeType(input, ["array"])) return input.slice(beginIndex, endIndex);
5456
+ else throw new RuntimeError(`Expected first argument to be of type array or string, but found ${typeToString(typeOf(input))} instead.`, this.key);
5404
5457
  }
5405
5458
  eachChild(fn) {
5406
5459
  fn(this.input);
5407
- for (const expression of this.outputs) fn(expression);
5460
+ fn(this.beginIndex);
5461
+ if (this.endIndex) fn(this.endIndex);
5408
5462
  }
5409
5463
  outputDefined() {
5410
- return this.outputs.every((out) => out.outputDefined());
5464
+ return false;
5411
5465
  }
5412
5466
  };
5413
- /**
5414
- * Returns a ratio that can be used to interpolate between exponential function
5415
- * stops.
5416
- * 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,
5417
- * and `a` and `b` are constants affording sufficient degrees of freedom to fit
5418
- * the function to the given stops.
5419
- *
5420
- * Here's a bit of algebra that lets us compute `f(x)` directly from the stop
5421
- * values without explicitly solving for `a` and `b`:
5422
- *
5423
- * First stop value: `f(x0) = y0 = a * base^x0 + b`
5424
- * Second stop value: `f(x1) = y1 = a * base^x1 + b`
5425
- * => `y1 - y0 = a(base^x1 - base^x0)`
5426
- * => `a = (y1 - y0)/(base^x1 - base^x0)`
5427
- *
5428
- * Desired value: `f(x) = y = a * base^x + b`
5429
- * => `f(x) = y0 + a * (base^x - base^x0)`
5430
- *
5431
- * From the above, we can replace the `a` in `a * (base^x - base^x0)` and do a
5432
- * little algebra:
5433
- * ```
5434
- * a * (base^x - base^x0) = (y1 - y0)/(base^x1 - base^x0) * (base^x - base^x0)
5435
- * = (y1 - y0) * (base^x - base^x0) / (base^x1 - base^x0)
5436
- * ```
5437
- *
5438
- * If we let `(base^x - base^x0) / (base^x1 base^x0)`, then we have
5439
- * `f(x) = y0 + (y1 - y0) * ratio`. In other words, `ratio` may be treated as
5440
- * an interpolation factor between the two stops' output values.
5441
- *
5442
- * (Note: a slightly different form for `ratio`,
5443
- * `(base^(x-x0) - 1) / (base^(x1-x0) - 1) `, is equivalent, but requires fewer
5444
- * expensive `Math.pow()` operations.)
5445
- *
5446
- * @private
5447
- */
5448
- function exponentialInterpolation(input, base, lowerValue, upperValue) {
5449
- const difference = upperValue - lowerValue;
5450
- const progress = input - lowerValue;
5451
- if (difference === 0) return 0;
5452
- else if (base === 1) return progress / difference;
5453
- else return (Math.pow(base, progress) - 1) / (Math.pow(base, difference) - 1);
5454
- }
5455
- const interpolateFactory = {
5456
- color: Color$1.interpolate,
5457
- number: interpolateNumber,
5458
- padding: Padding.interpolate,
5459
- numberArray: NumberArray.interpolate,
5460
- colorArray: ColorArray.interpolate,
5461
- variableAnchorOffsetCollection: VariableAnchorOffsetCollection.interpolate,
5462
- array: interpolateArray
5463
- };
5464
- //#endregion
5467
+
5465
5468
  //#region src/expression/definitions/coalesce.ts
5466
5469
  var Coalesce = class Coalesce {
5467
5470
  constructor(type, args) {
@@ -5506,7 +5509,7 @@
5506
5509
  return this.args.every((arg) => arg.outputDefined());
5507
5510
  }
5508
5511
  };
5509
- //#endregion
5512
+
5510
5513
  //#region src/expression/definitions/comparison.ts
5511
5514
  function isComparableType(op, type) {
5512
5515
  if (op === "==" || op === "!=") return type.kind === "boolean" || type.kind === "string" || type.kind === "number" || type.kind === "null" || type.kind === "value";
@@ -5629,7 +5632,7 @@
5629
5632
  const GreaterThan = makeComparison(">", gt, gtCollate);
5630
5633
  const LessThanOrEqual = makeComparison("<=", lteq, lteqCollate);
5631
5634
  const GreaterThanOrEqual = makeComparison(">=", gteq, gteqCollate);
5632
- //#endregion
5635
+
5633
5636
  //#region src/expression/definitions/collator.ts
5634
5637
  var CollatorExpression = class CollatorExpression {
5635
5638
  constructor(caseSensitive, diacriticSensitive, locale) {
@@ -5665,7 +5668,7 @@
5665
5668
  return false;
5666
5669
  }
5667
5670
  };
5668
- //#endregion
5671
+
5669
5672
  //#region src/expression/definitions/number_format.ts
5670
5673
  var NumberFormat = class NumberFormat {
5671
5674
  constructor(number, locale, currency, unit, minFractionDigits, maxFractionDigits) {
@@ -5717,103 +5720,22 @@
5717
5720
  currency: this.currency ? this.currency.evaluate(ctx) : void 0,
5718
5721
  unit: this.unit ? this.unit.evaluate(ctx) : void 0,
5719
5722
  minimumFractionDigits: this.minFractionDigits ? this.minFractionDigits.evaluate(ctx) : void 0,
5720
- maximumFractionDigits: this.maxFractionDigits ? this.maxFractionDigits.evaluate(ctx) : void 0
5721
- }).format(this.number.evaluate(ctx));
5722
- }
5723
- eachChild(fn) {
5724
- fn(this.number);
5725
- if (this.locale) fn(this.locale);
5726
- if (this.currency) fn(this.currency);
5727
- if (this.unit) fn(this.unit);
5728
- if (this.minFractionDigits) fn(this.minFractionDigits);
5729
- if (this.maxFractionDigits) fn(this.maxFractionDigits);
5730
- }
5731
- outputDefined() {
5732
- return false;
5733
- }
5734
- };
5735
- //#endregion
5736
- //#region src/expression/definitions/format.ts
5737
- var FormatExpression = class FormatExpression {
5738
- constructor(sections) {
5739
- this.type = FormattedType;
5740
- this.sections = sections;
5741
- }
5742
- static parse(args, context) {
5743
- if (args.length < 2) return context.error("Expected at least one argument.");
5744
- const firstArg = args[1];
5745
- if (!Array.isArray(firstArg) && typeof firstArg === "object") return context.error("First argument must be an image or text section.");
5746
- const sections = [];
5747
- let nextTokenMayBeObject = false;
5748
- for (let i = 1; i <= args.length - 1; ++i) {
5749
- const arg = args[i];
5750
- if (nextTokenMayBeObject && typeof arg === "object" && !Array.isArray(arg)) {
5751
- nextTokenMayBeObject = false;
5752
- let scale = null;
5753
- if (arg["font-scale"]) {
5754
- scale = context.parse(arg["font-scale"], 1, NumberType);
5755
- if (!scale) return null;
5756
- }
5757
- let font = null;
5758
- if (arg["text-font"]) {
5759
- font = context.parse(arg["text-font"], 1, array(StringType));
5760
- if (!font) return null;
5761
- }
5762
- let textColor = null;
5763
- if (arg["text-color"]) {
5764
- textColor = context.parse(arg["text-color"], 1, ColorType);
5765
- if (!textColor) return null;
5766
- }
5767
- let verticalAlign = null;
5768
- if (arg["vertical-align"]) {
5769
- 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.`);
5770
- verticalAlign = context.parse(arg["vertical-align"], 1, StringType);
5771
- if (!verticalAlign) return null;
5772
- }
5773
- const lastExpression = sections[sections.length - 1];
5774
- lastExpression.scale = scale;
5775
- lastExpression.font = font;
5776
- lastExpression.textColor = textColor;
5777
- lastExpression.verticalAlign = verticalAlign;
5778
- } else {
5779
- const content = context.parse(args[i], 1, ValueType);
5780
- if (!content) return null;
5781
- const kind = content.type.kind;
5782
- if (kind !== "string" && kind !== "value" && kind !== "null" && kind !== "resolvedImage") return context.error("Formatted text type must be 'string', 'value', 'image' or 'null'.");
5783
- nextTokenMayBeObject = true;
5784
- sections.push({
5785
- content,
5786
- scale: null,
5787
- font: null,
5788
- textColor: null,
5789
- verticalAlign: null
5790
- });
5791
- }
5792
- }
5793
- return new FormatExpression(sections);
5794
- }
5795
- evaluate(ctx) {
5796
- const evaluateSection = (section) => {
5797
- const evaluatedContent = section.content.evaluate(ctx);
5798
- if (typeOf(evaluatedContent) === ResolvedImageType) return new FormattedSection("", evaluatedContent, null, null, null, section.verticalAlign ? section.verticalAlign.evaluate(ctx) : null);
5799
- 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);
5800
- };
5801
- return new Formatted(this.sections.map(evaluateSection));
5723
+ maximumFractionDigits: this.maxFractionDigits ? this.maxFractionDigits.evaluate(ctx) : void 0
5724
+ }).format(this.number.evaluate(ctx));
5802
5725
  }
5803
5726
  eachChild(fn) {
5804
- for (const section of this.sections) {
5805
- fn(section.content);
5806
- if (section.scale) fn(section.scale);
5807
- if (section.font) fn(section.font);
5808
- if (section.textColor) fn(section.textColor);
5809
- if (section.verticalAlign) fn(section.verticalAlign);
5810
- }
5727
+ fn(this.number);
5728
+ if (this.locale) fn(this.locale);
5729
+ if (this.currency) fn(this.currency);
5730
+ if (this.unit) fn(this.unit);
5731
+ if (this.minFractionDigits) fn(this.minFractionDigits);
5732
+ if (this.maxFractionDigits) fn(this.maxFractionDigits);
5811
5733
  }
5812
5734
  outputDefined() {
5813
5735
  return false;
5814
5736
  }
5815
5737
  };
5816
- //#endregion
5738
+
5817
5739
  //#region src/expression/definitions/image.ts
5818
5740
  var ImageExpression = class ImageExpression {
5819
5741
  constructor(input) {
@@ -5839,7 +5761,7 @@
5839
5761
  return false;
5840
5762
  }
5841
5763
  };
5842
- //#endregion
5764
+
5843
5765
  //#region src/expression/definitions/length.ts
5844
5766
  var Length = class Length {
5845
5767
  constructor(input, key) {
@@ -5867,7 +5789,7 @@
5867
5789
  return false;
5868
5790
  }
5869
5791
  };
5870
- //#endregion
5792
+
5871
5793
  //#region src/util/geometry_util.ts
5872
5794
  const EXTENT = 8192;
5873
5795
  function getTileCoordinates(p, canonical) {
@@ -5963,7 +5885,7 @@
5963
5885
  if (det1 > 0 && det2 < 0 || det1 < 0 && det2 > 0) return true;
5964
5886
  return false;
5965
5887
  }
5966
- //#endregion
5888
+
5967
5889
  //#region src/expression/definitions/within.ts
5968
5890
  function getTilePolygon(coordinates, bbox, canonical) {
5969
5891
  const polygon = [];
@@ -6125,7 +6047,7 @@
6125
6047
  return true;
6126
6048
  }
6127
6049
  };
6128
- //#endregion
6050
+
6129
6051
  //#region node_modules/tinyqueue/index.js
6130
6052
  var TinyQueue = class {
6131
6053
  constructor(data = [], compare = (a, b) => a < b ? -1 : a > b ? 1 : 0) {
@@ -6178,54 +6100,10 @@
6178
6100
  data[pos] = item;
6179
6101
  }
6180
6102
  };
6181
- //#endregion
6182
- //#region src/util/classify_rings.ts
6183
- /**
6184
- * Classifies an array of rings into polygons with outer rings and holes
6185
- * @param rings - the rings to classify
6186
- * @param maxRings - the maximum number of rings to include in a polygon, use 0 to include all rings
6187
- * @returns an array of polygons with internal rings as holes
6188
- */
6189
- function classifyRings$1(rings, maxRings) {
6190
- if (rings.length <= 1) return [rings];
6191
- const polygons = [];
6192
- let polygon;
6193
- let ccw;
6194
- for (const ring of rings) {
6195
- const area = calculateSignedArea(ring);
6196
- if (area === 0) continue;
6197
- ring.area = Math.abs(area);
6198
- if (ccw === void 0) ccw = area < 0;
6199
- if (ccw === area < 0) {
6200
- if (polygon) polygons.push(polygon);
6201
- polygon = [ring];
6202
- } else polygon.push(ring);
6203
- }
6204
- if (polygon) polygons.push(polygon);
6205
- return polygons;
6206
- }
6207
- /**
6208
- * Returns the signed area for the polygon ring. Positive areas are exterior rings and
6209
- * have a clockwise winding. Negative areas are interior rings and have a counter clockwise
6210
- * ordering.
6211
- *
6212
- * @param ring - Exterior or interior ring
6213
- * @returns Signed area
6214
- */
6215
- function calculateSignedArea(ring) {
6216
- let sum = 0;
6217
- for (let i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) {
6218
- p1 = ring[i];
6219
- p2 = ring[j];
6220
- sum += (p2.x - p1.x) * (p1.y + p2.y);
6221
- }
6222
- return sum;
6223
- }
6224
- //#endregion
6103
+
6225
6104
  //#region src/util/cheap_ruler.ts
6226
6105
  const RE = 6378.137;
6227
- const FE = 1 / 298.257223563;
6228
- const E2 = FE * (2 - FE);
6106
+ const E2 = .0066943799901413165;
6229
6107
  const RAD = Math.PI / 180;
6230
6108
  var CheapRuler = class {
6231
6109
  constructor(lat) {
@@ -6234,7 +6112,7 @@
6234
6112
  const w2 = 1 / (1 - E2 * (1 - coslat * coslat));
6235
6113
  const w = Math.sqrt(w2);
6236
6114
  this.kx = m * w * coslat;
6237
- this.ky = m * w * w2 * (1 - E2);
6115
+ this.ky = m * w * w2 * .9933056200098587;
6238
6116
  }
6239
6117
  /**
6240
6118
  * Given two points of the form [longitude, latitude], returns the distance.
@@ -6305,7 +6183,7 @@
6305
6183
  return deg;
6306
6184
  }
6307
6185
  };
6308
- //#endregion
6186
+
6309
6187
  //#region src/expression/definitions/distance.ts
6310
6188
  const MinPointsSize = 100;
6311
6189
  const MinLinePointsSize = 50;
@@ -6573,9 +6451,7 @@
6573
6451
  case "LineString":
6574
6452
  dist = Math.min(dist, pointSetToPointSetDistance(pointPosition, false, geometry.coordinates, true, ruler, dist));
6575
6453
  break;
6576
- case "Polygon":
6577
- dist = Math.min(dist, pointsToPolygonDistance(pointPosition, false, geometry.coordinates, ruler, dist));
6578
- break;
6454
+ case "Polygon": dist = Math.min(dist, pointsToPolygonDistance(pointPosition, false, geometry.coordinates, ruler, dist));
6579
6455
  }
6580
6456
  if (dist === 0) return dist;
6581
6457
  }
@@ -6595,9 +6471,7 @@
6595
6471
  case "LineString":
6596
6472
  dist = Math.min(dist, pointSetToPointSetDistance(linePositions, true, geometry.coordinates, true, ruler, dist));
6597
6473
  break;
6598
- case "Polygon":
6599
- dist = Math.min(dist, pointsToPolygonDistance(linePositions, true, geometry.coordinates, ruler, dist));
6600
- break;
6474
+ case "Polygon": dist = Math.min(dist, pointsToPolygonDistance(linePositions, true, geometry.coordinates, ruler, dist));
6601
6475
  }
6602
6476
  if (dist === 0) return dist;
6603
6477
  }
@@ -6621,9 +6495,7 @@
6621
6495
  case "LineString":
6622
6496
  dist = Math.min(dist, pointsToPolygonDistance(geometry.coordinates, true, polygon, ruler, dist));
6623
6497
  break;
6624
- case "Polygon":
6625
- dist = Math.min(dist, polygonToPolygonDistance(polygon, geometry.coordinates, ruler, dist));
6626
- break;
6498
+ case "Polygon": dist = Math.min(dist, polygonToPolygonDistance(polygon, geometry.coordinates, ruler, dist));
6627
6499
  }
6628
6500
  if (dist === 0) return dist;
6629
6501
  }
@@ -6679,7 +6551,48 @@
6679
6551
  return true;
6680
6552
  }
6681
6553
  };
6682
- //#endregion
6554
+
6555
+ //#region src/expression/definitions/semiliteral.ts
6556
+ var Semiliteral = class Semiliteral {
6557
+ constructor(arr) {
6558
+ let elementType = null;
6559
+ for (const expr of arr) if (!elementType) elementType = expr.type;
6560
+ else if (elementType === expr.type) continue;
6561
+ else {
6562
+ elementType = ValueType;
6563
+ break;
6564
+ }
6565
+ this.type = array(elementType ?? ValueType, arr.length);
6566
+ this.arr = arr;
6567
+ }
6568
+ static parse(args, context) {
6569
+ if (args.length !== 2) return context.error(`'semiliteral' expression requires exactly one argument, but found ${args.length - 1} instead.`);
6570
+ if (!isValue(args[1])) return context.error(`invalid value of type "${typeof args[1]}"`);
6571
+ const value = args[1];
6572
+ const type = typeOf(value);
6573
+ if (type.kind === "array") {
6574
+ const arr = value;
6575
+ const arrayContext = context.concat(1);
6576
+ const parsed = [];
6577
+ for (let i = 0; i < arr.length; i++) {
6578
+ const item = arrayContext.parse(arr[i], i, ValueType);
6579
+ if (!item) return null;
6580
+ parsed.push(item);
6581
+ }
6582
+ return new Semiliteral(parsed);
6583
+ } else return new Literal(type, value);
6584
+ }
6585
+ evaluate(ctx) {
6586
+ return this.arr.map((arg) => arg.evaluate(ctx));
6587
+ }
6588
+ eachChild(fn) {
6589
+ this.arr.forEach(fn);
6590
+ }
6591
+ outputDefined() {
6592
+ return this.arr.every((arg) => arg.outputDefined());
6593
+ }
6594
+ };
6595
+
6683
6596
  //#region src/expression/definitions/global_state.ts
6684
6597
  var GlobalState = class GlobalState {
6685
6598
  constructor(key) {
@@ -6703,7 +6616,7 @@
6703
6616
  return false;
6704
6617
  }
6705
6618
  };
6706
- //#endregion
6619
+
6707
6620
  //#region src/expression/definitions/index.ts
6708
6621
  const expressions = {
6709
6622
  "==": Equals,
@@ -6732,6 +6645,7 @@
6732
6645
  number: Assertion,
6733
6646
  "number-format": NumberFormat,
6734
6647
  object: Assertion,
6648
+ semiliteral: Semiliteral,
6735
6649
  slice: Slice,
6736
6650
  step: Step,
6737
6651
  string: Assertion,
@@ -6744,7 +6658,163 @@
6744
6658
  distance: Distance,
6745
6659
  "global-state": GlobalState
6746
6660
  };
6747
- //#endregion
6661
+
6662
+ //#region src/expression/parsing_error.ts
6663
+ var ExpressionParsingError = class extends Error {
6664
+ constructor(key, message) {
6665
+ super(message);
6666
+ this.message = message;
6667
+ this.key = key;
6668
+ }
6669
+ };
6670
+
6671
+ //#region src/expression/scope.ts
6672
+ /**
6673
+ * Tracks `let` bindings during expression parsing.
6674
+ * @private
6675
+ */
6676
+ var Scope = class Scope {
6677
+ constructor(parent, bindings = []) {
6678
+ this.parent = parent;
6679
+ this.bindings = {};
6680
+ for (const [name, expression] of bindings) this.bindings[name] = expression;
6681
+ }
6682
+ concat(bindings) {
6683
+ return new Scope(this, bindings);
6684
+ }
6685
+ get(name) {
6686
+ if (this.bindings[name]) return this.bindings[name];
6687
+ if (this.parent) return this.parent.get(name);
6688
+ throw new Error(`${name} not found in scope.`);
6689
+ }
6690
+ has(name) {
6691
+ if (this.bindings[name]) return true;
6692
+ return this.parent ? this.parent.has(name) : false;
6693
+ }
6694
+ };
6695
+
6696
+ //#region src/expression/parsing_context.ts
6697
+ /**
6698
+ * State associated parsing at a given point in an expression tree.
6699
+ * @private
6700
+ */
6701
+ var ParsingContext = class ParsingContext {
6702
+ constructor(registry, isConstantFunc, path = [], expectedType, scope = new Scope(), errors = []) {
6703
+ this.registry = registry;
6704
+ this.path = path;
6705
+ this.key = path.map((part) => `[${part}]`).join("");
6706
+ this.scope = scope;
6707
+ this.errors = errors;
6708
+ this.expectedType = expectedType;
6709
+ this._isConstant = isConstantFunc;
6710
+ }
6711
+ /**
6712
+ * @param expr the JSON expression to parse
6713
+ * @param index the optional argument index if this expression is an argument of a parent expression that's being parsed
6714
+ * @param options
6715
+ * @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.
6716
+ * @private
6717
+ */
6718
+ parse(expr, index, expectedType, bindings, options = {}) {
6719
+ if (index != null) return this.concat(index, expectedType, bindings)._parse(expr, options);
6720
+ return this._parse(expr, options);
6721
+ }
6722
+ _parse(expr, options) {
6723
+ if (expr === null || typeof expr === "string" || typeof expr === "boolean" || typeof expr === "number") expr = ["literal", expr];
6724
+ const key = this.key;
6725
+ function annotate(parsed, type, typeAnnotation) {
6726
+ if (typeAnnotation === "assert") return new Assertion(type, [parsed], key);
6727
+ else if (typeAnnotation === "coerce") return new Coercion(type, [parsed], key);
6728
+ else return parsed;
6729
+ }
6730
+ if (Array.isArray(expr)) {
6731
+ if (expr.length === 0) return this.error("Expected an array with at least one element. If you wanted a literal array, use [\"literal\", []].");
6732
+ const op = expr[0];
6733
+ if (typeof op !== "string") {
6734
+ this.error(`Expression name must be a string, but found ${typeof op} instead. If you wanted a literal array, use ["literal", [...]].`, 0);
6735
+ return null;
6736
+ }
6737
+ const Expr = this.registry[op];
6738
+ if (Expr) {
6739
+ let parsed = Expr.parse(expr, this);
6740
+ if (!parsed) return null;
6741
+ if (this.expectedType) {
6742
+ const expected = this.expectedType;
6743
+ const actual = parsed.type;
6744
+ 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");
6745
+ else if ("projectionDefinition" === expected.kind && [
6746
+ "string",
6747
+ "array",
6748
+ "value"
6749
+ ].includes(actual.kind) || [
6750
+ "color",
6751
+ "formatted",
6752
+ "resolvedImage"
6753
+ ].includes(expected.kind) && ["value", "string"].includes(actual.kind) || ["padding", "numberArray"].includes(expected.kind) && [
6754
+ "value",
6755
+ "number",
6756
+ "array"
6757
+ ].includes(actual.kind) || "colorArray" === expected.kind && [
6758
+ "value",
6759
+ "string",
6760
+ "array"
6761
+ ].includes(actual.kind) || "variableAnchorOffsetCollection" === expected.kind && ["value", "array"].includes(actual.kind)) parsed = annotate(parsed, expected, options.typeAnnotation || "coerce");
6762
+ else if (this.checkSubtype(expected, actual)) return null;
6763
+ }
6764
+ if (!(parsed instanceof Literal) && parsed.type.kind !== "resolvedImage" && this._isConstant(parsed)) {
6765
+ const ec = new EvaluationContext();
6766
+ try {
6767
+ parsed = new Literal(parsed.type, parsed.evaluate(ec));
6768
+ } catch (e) {
6769
+ this.error(e.message);
6770
+ return null;
6771
+ }
6772
+ }
6773
+ return parsed;
6774
+ }
6775
+ return this.error(`Unknown expression "${op}". If you wanted a literal array, use ["literal", [...]].`, 0);
6776
+ } else if (typeof expr === "undefined") return this.error("'undefined' value invalid. Use null instead.");
6777
+ else if (typeof expr === "object") return this.error("Bare objects invalid. Use [\"literal\", {...}] instead.");
6778
+ else return this.error(`Expected an array, but found ${typeof expr} instead.`);
6779
+ }
6780
+ /**
6781
+ * Returns a copy of this context suitable for parsing the subexpression at
6782
+ * index `index`, optionally appending to 'let' binding map.
6783
+ *
6784
+ * Note that `errors` property, intended for collecting errors while
6785
+ * parsing, is copied by reference rather than cloned.
6786
+ * @private
6787
+ */
6788
+ concat(index, expectedType, bindings) {
6789
+ const path = typeof index === "number" ? this.path.concat(index) : this.path;
6790
+ const scope = bindings ? this.scope.concat(bindings) : this.scope;
6791
+ return new ParsingContext(this.registry, this._isConstant, path, expectedType || null, scope, this.errors);
6792
+ }
6793
+ /**
6794
+ * Push a parsing (or type checking) error into the `this.errors`
6795
+ * @param error The message
6796
+ * @param keys Optionally specify the source of the error at a child
6797
+ * of the current expression at `this.key`.
6798
+ * @private
6799
+ */
6800
+ error(error, ...keys) {
6801
+ const key = `${this.key}${keys.map((k) => `[${k}]`).join("")}`;
6802
+ this.errors.push(new ExpressionParsingError(key, error));
6803
+ }
6804
+ /**
6805
+ * Returns null if `t` is a subtype of `expected`; otherwise returns an
6806
+ * error message and also pushes it to `this.errors`.
6807
+ * @param expected The expected type
6808
+ * @param t The actual type
6809
+ * @returns null if `t` is a subtype of `expected`; otherwise returns an error message
6810
+ */
6811
+ checkSubtype(expected, t) {
6812
+ const error = checkSubtype(expected, t);
6813
+ if (error) this.error(error);
6814
+ return error;
6815
+ }
6816
+ };
6817
+
6748
6818
  //#region src/expression/compound_expression.ts
6749
6819
  var CompoundExpression = class CompoundExpression {
6750
6820
  constructor(name, type, evaluate, args, key) {
@@ -7312,28 +7382,14 @@
7312
7382
  return result;
7313
7383
  }
7314
7384
  function isGlobalPropertyConstant(e, properties) {
7315
- if (e instanceof CompoundExpression && properties.indexOf(e.name) >= 0) return false;
7316
- let result = true;
7317
- e.eachChild((arg) => {
7318
- if (result && !isGlobalPropertyConstant(arg, properties)) result = false;
7319
- });
7320
- return result;
7321
- }
7322
- //#endregion
7323
- //#region src/util/result.ts
7324
- function success(value) {
7325
- return {
7326
- result: "success",
7327
- value
7328
- };
7329
- }
7330
- function error(value) {
7331
- return {
7332
- result: "error",
7333
- value
7334
- };
7385
+ if (e instanceof CompoundExpression && properties.indexOf(e.name) >= 0) return false;
7386
+ let result = true;
7387
+ e.eachChild((arg) => {
7388
+ if (result && !isGlobalPropertyConstant(arg, properties)) result = false;
7389
+ });
7390
+ return result;
7335
7391
  }
7336
- //#endregion
7392
+
7337
7393
  //#region src/util/properties.ts
7338
7394
  function supportsPropertyExpression(spec) {
7339
7395
  return spec["property-type"] === "data-driven" || spec["property-type"] === "cross-faded-data-driven";
@@ -7344,13 +7400,13 @@
7344
7400
  function supportsInterpolation(spec) {
7345
7401
  return !!spec.expression && spec.expression.interpolated;
7346
7402
  }
7347
- //#endregion
7403
+
7348
7404
  //#region src/util/extend.ts
7349
7405
  function extendBy(output, ...inputs) {
7350
7406
  for (const input of inputs) for (const k in input) output[k] = input[k];
7351
7407
  return output;
7352
7408
  }
7353
- //#endregion
7409
+
7354
7410
  //#region src/util/get_type.ts
7355
7411
  function getType(val) {
7356
7412
  if (val instanceof Number) return "number";
@@ -7360,7 +7416,7 @@
7360
7416
  else if (val === null) return "null";
7361
7417
  else return typeof val;
7362
7418
  }
7363
- //#endregion
7419
+
7364
7420
  //#region src/function/index.ts
7365
7421
  function isFunction(value) {
7366
7422
  return typeof value === "object" && value !== null && !Array.isArray(value) && typeOf(value) === ObjectType;
@@ -7458,21 +7514,21 @@
7458
7514
  kind: "source",
7459
7515
  evaluate(_, feature) {
7460
7516
  const value = feature && feature.properties ? feature.properties[parameters.property] : void 0;
7461
- if (value === void 0) return coalesce$1(parameters.default, propertySpec.default);
7517
+ if (value === void 0) return coalesce(parameters.default, propertySpec.default);
7462
7518
  return innerFun(parameters, propertySpec, value, hashedStops, categoricalKeyType);
7463
7519
  }
7464
7520
  };
7465
7521
  }
7466
- function coalesce$1(a, b, c) {
7522
+ function coalesce(a, b, c) {
7467
7523
  if (a !== void 0) return a;
7468
7524
  if (b !== void 0) return b;
7469
7525
  if (c !== void 0) return c;
7470
7526
  }
7471
7527
  function evaluateCategoricalFunction(parameters, propertySpec, input, hashedStops, keyType) {
7472
- return coalesce$1(typeof input === keyType ? hashedStops[input] : void 0, parameters.default, propertySpec.default);
7528
+ return coalesce(typeof input === keyType ? hashedStops[input] : void 0, parameters.default, propertySpec.default);
7473
7529
  }
7474
7530
  function evaluateIntervalFunction(parameters, propertySpec, input) {
7475
- if (getType(input) !== "number") return coalesce$1(parameters.default, propertySpec.default);
7531
+ if (getType(input) !== "number") return coalesce(parameters.default, propertySpec.default);
7476
7532
  const n = parameters.stops.length;
7477
7533
  if (n === 1) return parameters.stops[0][1];
7478
7534
  if (input <= parameters.stops[0][0]) return parameters.stops[0][1];
@@ -7482,13 +7538,13 @@
7482
7538
  }
7483
7539
  function evaluateExponentialFunction(parameters, propertySpec, input) {
7484
7540
  const base = parameters.base !== void 0 ? parameters.base : 1;
7485
- if (getType(input) !== "number") return coalesce$1(parameters.default, propertySpec.default);
7541
+ if (getType(input) !== "number") return coalesce(parameters.default, propertySpec.default);
7486
7542
  const n = parameters.stops.length;
7487
7543
  if (n === 1) return parameters.stops[0][1];
7488
7544
  if (input <= parameters.stops[0][0]) return parameters.stops[0][1];
7489
7545
  if (input >= parameters.stops[n - 1][0]) return parameters.stops[n - 1][1];
7490
7546
  const index = findStopLessThanOrEqualTo(parameters.stops.map((stop) => stop[0]), input, "");
7491
- const t = interpolationFactor(input, base, parameters.stops[index][0], parameters.stops[index + 1][0]);
7547
+ const t = interpolationFactor$1(input, base, parameters.stops[index][0], parameters.stops[index + 1][0]);
7492
7548
  const outputLower = parameters.stops[index][1];
7493
7549
  const outputUpper = parameters.stops[index + 1][1];
7494
7550
  const interp = interpolateFactory[propertySpec.type] || identityFunction;
@@ -7522,7 +7578,7 @@
7522
7578
  break;
7523
7579
  default: if (getType(input) !== propertySpec.type && (propertySpec.type !== "enum" || !propertySpec.values[input])) input = void 0;
7524
7580
  }
7525
- return coalesce$1(input, parameters.default, propertySpec.default);
7581
+ return coalesce(input, parameters.default, propertySpec.default);
7526
7582
  }
7527
7583
  /**
7528
7584
  * Returns a ratio that can be used to interpolate between exponential function
@@ -7562,14 +7618,28 @@
7562
7618
  *
7563
7619
  * @private
7564
7620
  */
7565
- function interpolationFactor(input, base, lowerValue, upperValue) {
7621
+ function interpolationFactor$1(input, base, lowerValue, upperValue) {
7566
7622
  const difference = upperValue - lowerValue;
7567
7623
  const progress = input - lowerValue;
7568
7624
  if (difference === 0) return 0;
7569
7625
  else if (base === 1) return progress / difference;
7570
7626
  else return (Math.pow(base, progress) - 1) / (Math.pow(base, difference) - 1);
7571
7627
  }
7572
- //#endregion
7628
+
7629
+ //#region src/util/result.ts
7630
+ function success(value) {
7631
+ return {
7632
+ result: "success",
7633
+ value
7634
+ };
7635
+ }
7636
+ function error(value) {
7637
+ return {
7638
+ result: "error",
7639
+ value
7640
+ };
7641
+ }
7642
+
7573
7643
  //#region src/expression/index.ts
7574
7644
  var StyleExpression = class {
7575
7645
  constructor(expression, rootKey, propertySpec, globalState) {
@@ -7851,7 +7921,7 @@
7851
7921
  globalState
7852
7922
  };
7853
7923
  }
7854
- //#endregion
7924
+
7855
7925
  //#region src/feature_filter/index.ts
7856
7926
  function classifyChildren(children) {
7857
7927
  let sawLegacy = false;
@@ -7957,12 +8027,10 @@
7957
8027
  if (diagnostic) return diagnostic;
7958
8028
  break;
7959
8029
  }
7960
- case "case":
7961
- for (let i = 1; i < filter.length - 1; i += 2) {
7962
- const diagnostic = checkChild(i, path, filter);
7963
- if (diagnostic) return diagnostic;
7964
- }
7965
- break;
8030
+ case "case": for (let i = 1; i < filter.length - 1; i += 2) {
8031
+ const diagnostic = checkChild(i, path, filter);
8032
+ if (diagnostic) return diagnostic;
8033
+ }
7966
8034
  }
7967
8035
  return null;
7968
8036
  }
@@ -8000,7 +8068,7 @@
8000
8068
  needGeometry: false,
8001
8069
  getGlobalStateRefs: () => /* @__PURE__ */ new Set()
8002
8070
  };
8003
- if (!isExpressionFilter(filter)) filter = convertFilter$1(filter);
8071
+ if (!isExpressionFilter(filter)) filter = convertFilter(filter);
8004
8072
  else warnAboutMixedLegacyFilter(filter, rootKey);
8005
8073
  const compiled = createExpression(filter, rootKey, filterSpec, globalState);
8006
8074
  if (compiled.result === "error") throw new Error(compiled.value.map((err) => `${err.key}: ${err.message}`).join(", "));
@@ -8019,13 +8087,13 @@
8019
8087
  for (let index = 1; index < filter.length; index++) if (geometryNeeded(filter[index])) return true;
8020
8088
  return false;
8021
8089
  }
8022
- function convertFilter$1(filter) {
8090
+ function convertFilter(filter) {
8023
8091
  if (!filter) return true;
8024
8092
  const op = filter[0];
8025
8093
  if (filter.length <= 1) return op !== "any";
8026
- 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;
8094
+ 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;
8027
8095
  }
8028
- function convertComparisonOp$1(property, value, op) {
8096
+ function convertComparisonOp(property, value, op) {
8029
8097
  switch (property) {
8030
8098
  case "$type": return [`filter-type-${op}`, value];
8031
8099
  case "$id": return [`filter-id-${op}`, value];
@@ -8037,9 +8105,9 @@
8037
8105
  }
8038
8106
  }
8039
8107
  function convertDisjunctionOp(filters) {
8040
- return ["any"].concat(filters.map(convertFilter$1));
8108
+ return ["any"].concat(filters.map(convertFilter));
8041
8109
  }
8042
- function convertInOp$1(property, values) {
8110
+ function convertInOp(property, values) {
8043
8111
  if (values.length === 0) return false;
8044
8112
  switch (property) {
8045
8113
  case "$type": return ["filter-type-in", ["literal", values]];
@@ -8056,7 +8124,7 @@
8056
8124
  ];
8057
8125
  }
8058
8126
  }
8059
- function convertHasOp$1(property) {
8127
+ function convertHasOp(property) {
8060
8128
  switch (property) {
8061
8129
  case "$type": return true;
8062
8130
  case "$id": return ["filter-has-id"];
@@ -8372,21 +8440,29 @@
8372
8440
  width;
8373
8441
  height;
8374
8442
  #svg;
8375
- #backgroundColor;
8376
8443
  #spriteSheetDefs = new Map();
8377
8444
  #spriteSymbolDefs = new Map();
8378
8445
  #sdfFilterDefs = new Map();
8379
8446
  #blurFilterDefs = new Map();
8447
+ #rasterDefs = [];
8448
+ #rasterClipCount = 0;
8449
+ #clipCircle;
8380
8450
  constructor(opt) {
8381
8451
  this.width = opt.width;
8382
8452
  this.height = opt.height;
8383
8453
  this.#svg = [];
8384
- this.#backgroundColor = Color.transparent;
8454
+ }
8455
+ setClipCircle(circle) {
8456
+ this.#clipCircle = circle;
8385
8457
  }
8386
8458
  drawBackgroundFill(style) {
8459
+ // Every background layer paints over what is below it, like any other layer. (A
8460
+ // transparent one, e.g. an empty "slot" layer, must not erase an earlier background.)
8387
8461
  const color = new Color(style.color);
8388
8462
  color.alpha *= style.opacity;
8389
- this.#backgroundColor = color;
8463
+ if (color.alpha <= 0)
8464
+ return;
8465
+ this.#svg.push(`<rect x="-1" y="-1" width="${(this.width + 2).toFixed(0)}" height="${(this.height + 2).toFixed(0)}" ${fillAttr(color)} />`);
8390
8466
  }
8391
8467
  drawPolygons(id, features) {
8392
8468
  if (features.length === 0)
@@ -8394,9 +8470,11 @@
8394
8470
  // Merge only *consecutive* features with identical attributes, so the paint
8395
8471
  // order of overlapping features matches MapLibre's source order. A global
8396
8472
  // merge would reorder interleaved features (e.g. data-driven fill colors).
8397
- // Fills and their antialias outlines are collected in one pass but emitted
8398
- // as two: MapLibre draws every fill first, then every outline, so a lower
8399
- // feature's outline composites on top of a later overlapping fill.
8473
+ // Translucent features are never merged: MapLibre blends each one separately,
8474
+ // so where they overlap, their opacity adds up — within one merged <path> it
8475
+ // would not. Fills and their antialias outlines are collected in one pass but
8476
+ // emitted as two: MapLibre draws every fill first, then every outline, so a
8477
+ // lower feature's outline composites on top of a later overlapping fill.
8400
8478
  const fillGroups = [];
8401
8479
  const outlineGroups = [];
8402
8480
  let currentFillKey;
@@ -8411,37 +8489,48 @@
8411
8489
  // Round each ring once; shared by the fill and the outline pass.
8412
8490
  const rings = feature.geometry.map((ring) => ring.map((p) => roundXY(p.x, p.y)));
8413
8491
  const color = new Color(style.color);
8492
+ const translucent = style.opacity < 1 || color.alpha < 255;
8414
8493
  if (color.alpha > 0) {
8415
8494
  const key = color.hex + translate + opacityAttr;
8416
- if (key !== currentFillKey) {
8495
+ if (translucent || key !== currentFillKey) {
8417
8496
  fillGroups.push({ segments: [], attrs: `${fillAttr(color)}${translate}${opacityAttr}` });
8418
- currentFillKey = key;
8497
+ currentFillKey = translucent ? undefined : key;
8419
8498
  }
8420
8499
  const group = fillGroups[fillGroups.length - 1];
8421
8500
  for (const ring of rings)
8422
8501
  group.segments.push(ring);
8423
8502
  }
8424
- // fill-antialias outline in an explicit fill-outline-color. MapLibre also
8425
- // draws a *default* outline in the fill color, but that outline IS its fill-
8426
- // edge antialiasing (draw_fill.ts clips it to just outside the shape) — and
8427
- // an SVG <path fill> is already rasterizer-antialiased, so redrawing it is
8428
- // redundant: measured identical pixel diff for +46–88% SVG size. We only
8429
- // draw the outline when a distinct color is set — the visible-border case
8430
- // (choropleths), which the browser's fill AA does not cover.
8431
- if (style.antialias && style.outlineColor !== undefined) {
8432
- const outlineColor = new Color(style.outlineColor);
8503
+ // fill-antialias outline, in fill-outline-color or else the fill color. For an
8504
+ // opaque fill, MapLibre's default outline IS its fill-edge antialiasing
8505
+ // (draw_fill.ts keeps it outside of the shape), and an SVG <path fill> is already
8506
+ // rasterizer-antialiased, so redrawing it is redundant: measured identical pixel
8507
+ // diff for +46–88% SVG size. It is drawn when it shows: in a distinct
8508
+ // fill-outline-color (choropleths), or for a translucent fill, where MapLibre
8509
+ // draws the outline over the fill's edge (e.g. buildings fading in).
8510
+ if (style.antialias && (style.outlineColor !== undefined || translucent)) {
8511
+ const outlineColor = style.outlineColor !== undefined ? new Color(style.outlineColor) : color;
8433
8512
  if (outlineColor.alpha > 0) {
8513
+ const outlineTranslucent = translucent || outlineColor.alpha < 255;
8434
8514
  const key = outlineColor.hex + translate + opacityAttr;
8435
- if (key !== currentOutlineKey) {
8515
+ if (outlineTranslucent || key !== currentOutlineKey) {
8436
8516
  outlineGroups.push({
8437
- segments: [],
8517
+ closed: [],
8518
+ open: [],
8438
8519
  attrs: `fill="none" ${strokeAttr(outlineColor, formatScaled(FILL_OUTLINE_WIDTH_PX))}${translate}${opacityAttr}`,
8439
8520
  });
8440
- currentOutlineKey = key;
8521
+ currentOutlineKey = outlineTranslucent ? undefined : key;
8441
8522
  }
8442
8523
  const group = outlineGroups[outlineGroups.length - 1];
8443
- for (const ring of rings)
8444
- group.segments.push(ring);
8524
+ // A polygon clipped to its tile has an outline without the clipped edges.
8525
+ if (feature.outline) {
8526
+ for (const line of feature.outline) {
8527
+ group.open.push(line.map((p) => roundXY(p.x, p.y)));
8528
+ }
8529
+ }
8530
+ else {
8531
+ for (const ring of rings)
8532
+ group.closed.push(ring);
8533
+ }
8445
8534
  }
8446
8535
  }
8447
8536
  });
@@ -8449,8 +8538,9 @@
8449
8538
  for (const { segments, attrs } of fillGroups) {
8450
8539
  this.#svg.push(`<path d="${segmentsToPath(segments, true)}" ${attrs} />`);
8451
8540
  }
8452
- for (const { segments, attrs } of outlineGroups) {
8453
- this.#svg.push(`<path d="${segmentsToPath(segments, true)}" ${attrs} />`);
8541
+ for (const { closed, open, attrs } of outlineGroups) {
8542
+ const d = segmentsToPath(closed, true) + segmentsToPath(open, false);
8543
+ this.#svg.push(`<path d="${d}" ${attrs} />`);
8454
8544
  }
8455
8545
  this.#svg.push('</g>');
8456
8546
  }
@@ -8458,7 +8548,12 @@
8458
8548
  if (features.length === 0)
8459
8549
  return;
8460
8550
  // Merge only *consecutive* same-attribute features (see drawPolygons) so the
8461
- // paint order of overlapping lines matches MapLibre's source order.
8551
+ // paint order of overlapping lines matches MapLibre's source order. Translucent
8552
+ // lines are never merged: MapLibre blends every line separately, so where lines
8553
+ // overlap (e.g. parallel rail tracks fading in), their opacity adds up — even
8554
+ // between the parts of one feature (tiles merge equal ways into one multi-line
8555
+ // feature). One <path> covers its overlaps only once, so a translucent line gets
8556
+ // a <path> per part.
8462
8557
  const groups = [];
8463
8558
  let currentKey;
8464
8559
  features.forEach(([feature, style]) => {
@@ -8493,7 +8588,8 @@
8493
8588
  translate,
8494
8589
  filterAttr,
8495
8590
  ].join('\0');
8496
- if (key !== currentKey) {
8591
+ const translucent = effectiveOpacity < 1 || color.alpha < 255;
8592
+ if (translucent || key !== currentKey) {
8497
8593
  const attrs = [
8498
8594
  'fill="none"',
8499
8595
  strokeAttr(color, roundedWidth),
@@ -8506,8 +8602,9 @@
8506
8602
  groups.push({
8507
8603
  segments: [],
8508
8604
  attrs: attrs.join(' ') + translate + opacityAttr + filterAttr,
8605
+ separate: translucent,
8509
8606
  });
8510
- currentKey = key;
8607
+ currentKey = translucent ? undefined : key;
8511
8608
  }
8512
8609
  const group = groups[groups.length - 1];
8513
8610
  feature.geometry.forEach((line) => {
@@ -8516,7 +8613,13 @@
8516
8613
  });
8517
8614
  });
8518
8615
  this.#svg.push(`<g id="${escapeXml(id)}">`);
8519
- for (const { segments, attrs } of groups) {
8616
+ for (const { segments, attrs, separate } of groups) {
8617
+ if (separate) {
8618
+ for (const segment of segments) {
8619
+ this.#svg.push(`<path d="${segmentsToPath([segment])}" ${attrs} />`);
8620
+ }
8621
+ continue;
8622
+ }
8520
8623
  const chains = chainSegments(segments);
8521
8624
  const d = segmentsToPath(chains);
8522
8625
  this.#svg.push(`<path d="${d}" ${attrs} />`);
@@ -8773,7 +8876,10 @@
8773
8876
  gAttrs += ` filter="${filters.join(' ')}"`;
8774
8877
  this.#svg.push(`<g ${gAttrs}>`);
8775
8878
  const pixelated = style.resampling === 'nearest';
8879
+ this.#drawRasterMeshes(tiles, pixelated);
8776
8880
  for (const tile of tiles) {
8881
+ if (tile.triangles)
8882
+ continue;
8777
8883
  const overlap = Math.min(tile.width, tile.height) / 10000; // slight overlap to prevent sub-pixel gaps between tiles
8778
8884
  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}"`;
8779
8885
  if (pixelated)
@@ -8782,11 +8888,65 @@
8782
8888
  }
8783
8889
  this.#svg.push('</g>');
8784
8890
  }
8891
+ /**
8892
+ * Draws raster tiles given as meshes of triangles (globe projection). Every tile image is
8893
+ * defined once (as a square of RASTER_TILE_UNITS); every triangle shows it through the affine
8894
+ * transform that maps its three source corners exactly onto its three screen corners, clipped
8895
+ * to the triangle on screen.
8896
+ *
8897
+ * Seams: within a tile, each clip triangle is grown by RASTER_TRIANGLE_OVERLAP_PX; the same
8898
+ * transform continues there, so the overlap shows (almost) the same pixels as the neighbour.
8899
+ * At tile borders that is not enough, as both images end on the same line and their anti-
8900
+ * aliased edges let a hairline of the background shine through. So first, as an underlay,
8901
+ * the triangles along the tile borders are drawn with their image reaching beyond the border
8902
+ * (see {@link bleedAtTileBorder}); then all triangles are drawn exactly on top of it. The
8903
+ * slightly shifted underlay only shows through the seam.
8904
+ */
8905
+ #drawRasterMeshes(tiles, pixelated) {
8906
+ const meshes = [];
8907
+ for (const tile of tiles) {
8908
+ if (!tile.triangles)
8909
+ continue;
8910
+ const imageId = `raster-${String(this.#rasterDefs.length)}`;
8911
+ const size = String(RASTER_TILE_UNITS);
8912
+ let imageAttrs = `id="${imageId}" width="${size}" height="${size}" preserveAspectRatio="none" xlink:href="${tile.dataUri}"`;
8913
+ if (pixelated)
8914
+ imageAttrs += ' style="image-rendering:pixelated"';
8915
+ this.#rasterDefs.push(`<image ${imageAttrs} />`);
8916
+ meshes.push({ imageId, triangles: tile.triangles });
8917
+ }
8918
+ for (const { imageId, triangles } of meshes) {
8919
+ for (const { source, target } of triangles) {
8920
+ if (!isOnTileBorder(source))
8921
+ continue;
8922
+ this.#drawRasterTriangle(imageId, bleedAtTileBorder(source, target), target);
8923
+ }
8924
+ }
8925
+ for (const { imageId, triangles } of meshes) {
8926
+ for (const { source, target } of triangles) {
8927
+ this.#drawRasterTriangle(imageId, source, target);
8928
+ }
8929
+ }
8930
+ }
8931
+ #drawRasterTriangle(imageId, source, target) {
8932
+ const matrix = affineFromTriangles(source, target);
8933
+ if (!matrix)
8934
+ return;
8935
+ const clip = growTriangle(target, RASTER_TRIANGLE_OVERLAP_PX);
8936
+ const clipId = `raster-clip-${String(this.#rasterClipCount++)}`;
8937
+ const d = segmentsToPath([clip.map(([x, y]) => roundXY(x, y))], true);
8938
+ this.#svg.push(`<clipPath id="${clipId}"><path d="${d}"/></clipPath>` +
8939
+ `<g clip-path="url(#${clipId})"><use xlink:href="#${imageId}" transform="matrix(${matrix.map(formatUnit).join(',')})" /></g>`);
8940
+ }
8785
8941
  getString() {
8786
8942
  const w = this.width.toFixed(0);
8787
8943
  const h = this.height.toFixed(0);
8788
8944
  // Build defs content
8789
- const defsContent = [`<clipPath id="vb"><rect width="${w}" height="${h}"/></clipPath>`];
8945
+ const clip = this.#clipCircle;
8946
+ const clipShape = clip
8947
+ ? `<circle cx="${formatUnit(clip.x)}" cy="${formatUnit(clip.y)}" r="${formatUnit(clip.radius)}"/>`
8948
+ : `<rect width="${w}" height="${h}"/>`;
8949
+ const defsContent = [`<clipPath id="vb">${clipShape}</clipPath>`, ...this.#rasterDefs];
8790
8950
  for (const sheet of this.#spriteSheetDefs.values()) {
8791
8951
  defsContent.push(`<image id="${escapeXml(sheet.defId)}" width="${formatNum(sheet.width)}" height="${formatNum(sheet.height)}" xlink:href="${escapeXml(sheet.href)}" />`);
8792
8952
  }
@@ -8813,9 +8973,6 @@
8813
8973
  `<defs>\n ${defsContent.join('\n ')}\n</defs>`,
8814
8974
  `<g id="map" clip-path="url(#vb)">`,
8815
8975
  ];
8816
- if (this.#backgroundColor.alpha > 0) {
8817
- parts.push(`<rect x="-1" y="-1" width="${(this.width + 2).toFixed(0)}" height="${(this.height + 2).toFixed(0)}" ${fillAttr(this.#backgroundColor)} />`);
8818
- }
8819
8976
  parts.push(...this.#svg, '</g>', '</svg>');
8820
8977
  return parts.join('\n');
8821
8978
  }
@@ -8835,6 +8992,85 @@
8835
8992
  function formatScaled(v) {
8836
8993
  return formatNum(Math.round(v * 10));
8837
8994
  }
8995
+ /** Size of a raster tile image in user units, when drawn as a mesh of triangles on the globe. */
8996
+ const RASTER_TILE_UNITS = 256;
8997
+ /** How far (pixels) a raster tile image reaches beyond the tile border in the seam underlay. */
8998
+ const RASTER_TILE_BLEED_PX = 1;
8999
+ /** How far (pixels) each raster triangle is grown beyond its edges to overlap its neighbours. */
9000
+ const RASTER_TRIANGLE_OVERLAP_PX = 0.5;
9001
+ /**
9002
+ * The affine transform [a, b, c, d, e, f] mapping the source triangle (in tile units 0..1,
9003
+ * scaled to RASTER_TILE_UNITS) exactly onto the target triangle, or undefined if either is
9004
+ * degenerate.
9005
+ */
9006
+ function affineFromTriangles(source, target) {
9007
+ const k = RASTER_TILE_UNITS;
9008
+ const [[u0, v0], [u1, v1], [u2, v2]] = source.map(([u, v]) => [u * k, v * k]);
9009
+ const [[x0, y0], [x1, y1], [x2, y2]] = target;
9010
+ const du1 = u1 - u0;
9011
+ const dv1 = v1 - v0;
9012
+ const du2 = u2 - u0;
9013
+ const dv2 = v2 - v0;
9014
+ const det = du1 * dv2 - du2 * dv1;
9015
+ if (Math.abs(det) < 1e-12)
9016
+ return undefined;
9017
+ const dx1 = x1 - x0;
9018
+ const dx2 = x2 - x0;
9019
+ const dy1 = y1 - y0;
9020
+ const dy2 = y2 - y0;
9021
+ // Screen area below a hundredth of a pixel: nothing to draw.
9022
+ if (Math.abs(dx1 * dy2 - dx2 * dy1) < 0.02)
9023
+ return undefined;
9024
+ const a = (dx1 * dv2 - dx2 * dv1) / det;
9025
+ const c = (du1 * dx2 - du2 * dx1) / det;
9026
+ const b = (dy1 * dv2 - dy2 * dv1) / det;
9027
+ const d = (du1 * dy2 - du2 * dy1) / det;
9028
+ return [a, b, c, d, x0 - a * u0 - c * v0, y0 - b * u0 - d * v0];
9029
+ }
9030
+ function isOnTileBorder(source) {
9031
+ return source.some(([u, v]) => u === 0 || u === 1 || v === 0 || v === 1);
9032
+ }
9033
+ /**
9034
+ * Moves the source corners of a triangle that lie on the tile border inwards, by the
9035
+ * equivalent of RASTER_TILE_BLEED_PX on screen, so that the image edge lies beyond the border.
9036
+ * (Used for the underlay that closes the seams between tiles, see `#drawRasterMeshes`.)
9037
+ */
9038
+ function bleedAtTileBorder(source, target) {
9039
+ const [[u0, v0], [u1, v1], [u2, v2]] = source;
9040
+ const [[x0, y0], [x1, y1], [x2, y2]] = target;
9041
+ const sourceArea = Math.abs((u1 - u0) * (v2 - v0) - (u2 - u0) * (v1 - v0));
9042
+ const targetArea = Math.abs((x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0));
9043
+ if (sourceArea === 0 || targetArea === 0)
9044
+ return source;
9045
+ // Pixels per tile unit, and the bleed in tile units, kept within the triangle.
9046
+ const scale = Math.sqrt(targetArea / sourceArea);
9047
+ const size = Math.max(Math.abs(u1 - u0), Math.abs(u2 - u0), Math.abs(v1 - v0), Math.abs(v2 - v0));
9048
+ const bleed = Math.min(RASTER_TILE_BLEED_PX / scale, size / 4);
9049
+ const inset = (t) => (t === 0 ? bleed : t === 1 ? 1 - bleed : t);
9050
+ return source.map(([u, v]) => [inset(u), inset(v)]);
9051
+ }
9052
+ /**
9053
+ * Moves every edge of a triangle outwards by `distance`, by scaling it around its incenter.
9054
+ * Growth is limited for very thin triangles, whose corners would otherwise shoot far out.
9055
+ */
9056
+ function growTriangle(triangle, distance) {
9057
+ const [[x0, y0], [x1, y1], [x2, y2]] = triangle;
9058
+ const a = Math.hypot(x2 - x1, y2 - y1);
9059
+ const b = Math.hypot(x2 - x0, y2 - y0);
9060
+ const c = Math.hypot(x1 - x0, y1 - y0);
9061
+ const perimeter = a + b + c;
9062
+ const area = Math.abs((x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0)) / 2;
9063
+ if (perimeter === 0 || area === 0)
9064
+ return triangle;
9065
+ const inradius = (2 * area) / perimeter;
9066
+ const cx = (a * x0 + b * x1 + c * x2) / perimeter;
9067
+ const cy = (a * y0 + b * y1 + c * y2) / perimeter;
9068
+ const scale = 1 + distance / Math.max(inradius, 2 * distance);
9069
+ return triangle.map(([x, y]) => [cx + (x - cx) * scale, cy + (y - cy) * scale]);
9070
+ }
9071
+ function formatUnit(v) {
9072
+ return (Math.round(v * 100000) / 100000).toString();
9073
+ }
8838
9074
  function formatScale(v) {
8839
9075
  return (Math.round(v * 10000) / 10000).toString();
8840
9076
  }
@@ -8928,6 +9164,12 @@
8928
9164
  properties;
8929
9165
  patterns;
8930
9166
  geometry;
9167
+ /**
9168
+ * The boundary of a polygon for the fill-antialias outline, as open polylines — when it
9169
+ * differs from the polygon's rings, i.e. when the polygon was clipped to its tile: the
9170
+ * clipped edges along the tile border are not part of the outline.
9171
+ */
9172
+ outline;
8931
9173
  #bbox;
8932
9174
  constructor(opt) {
8933
9175
  this.type = opt.type;
@@ -8935,6 +9177,7 @@
8935
9177
  this.properties = opt.properties;
8936
9178
  this.patterns = opt.patterns;
8937
9179
  this.geometry = opt.geometry;
9180
+ this.outline = opt.outline;
8938
9181
  }
8939
9182
  getBbox() {
8940
9183
  if (this.#bbox)
@@ -10029,6 +10272,138 @@
10029
10272
  return str;
10030
10273
  }
10031
10274
 
10275
+ /**
10276
+ * Clipping of vector tile geometry to the tile's own square.
10277
+ *
10278
+ * Vector tiles contain a buffer: geometry reaches a little beyond the tile, so a polygon on
10279
+ * a tile border exists in both tiles. MapLibre draws each tile clipped to its square (with the
10280
+ * stencil buffer), so nothing is drawn twice. Opaque fills hide the difference, translucent
10281
+ * ones do not: the overlap would be drawn with double opacity.
10282
+ */
10283
+ /** Whether any point lies outside of the square [min, max]². */
10284
+ function exceedsSquare(rings, min, max) {
10285
+ return rings.some((ring) => ring.some(({ x, y }) => x < min || x > max || y < min || y > max));
10286
+ }
10287
+ /**
10288
+ * Clips the rings of a polygon to the square [min, max]² (Sutherland–Hodgman). Rings that
10289
+ * vanish are dropped.
10290
+ */
10291
+ function clipPolygon(rings, min, max) {
10292
+ const result = [];
10293
+ for (const ring of rings) {
10294
+ let points = ring;
10295
+ points = clipAgainst(points, (p) => p.x >= min, (a, b) => lerpAtX(a, b, min));
10296
+ points = clipAgainst(points, (p) => p.x <= max, (a, b) => lerpAtX(a, b, max));
10297
+ points = clipAgainst(points, (p) => p.y >= min, (a, b) => lerpAtY(a, b, min));
10298
+ points = clipAgainst(points, (p) => p.y <= max, (a, b) => lerpAtY(a, b, max));
10299
+ if (points.length >= 3)
10300
+ result.push(points);
10301
+ }
10302
+ return result;
10303
+ }
10304
+ /**
10305
+ * The boundary of a polygon within the square [min, max]², as open polylines: the parts of
10306
+ * its rings inside the square, without the edges the clipping adds along the square's border.
10307
+ * (MapLibre's fill outline follows the original rings, cut off at the tile border.)
10308
+ */
10309
+ function clipPolygonOutline(rings, min, max) {
10310
+ const result = [];
10311
+ for (const ring of rings) {
10312
+ if (ring.length < 2)
10313
+ continue;
10314
+ const first = ring[0];
10315
+ const last = ring[ring.length - 1];
10316
+ const closed = first.x === last.x && first.y === last.y ? ring : [...ring, first];
10317
+ result.push(...clipLine(closed, min, max));
10318
+ }
10319
+ return result;
10320
+ }
10321
+ /** Clips a polyline to the square [min, max]², returning the parts inside of it. */
10322
+ function clipLine(line, min, max) {
10323
+ const parts = [];
10324
+ let current = [];
10325
+ for (let i = 0; i + 1 < line.length; i++) {
10326
+ const segment = clipSegment(line[i], line[i + 1], min, max);
10327
+ if (!segment) {
10328
+ if (current.length > 1)
10329
+ parts.push(current);
10330
+ current = [];
10331
+ continue;
10332
+ }
10333
+ const [a, b] = segment;
10334
+ const last = current[current.length - 1];
10335
+ if (last?.x !== a.x || last.y !== a.y) {
10336
+ if (current.length > 1)
10337
+ parts.push(current);
10338
+ current = [a];
10339
+ }
10340
+ current.push(b);
10341
+ // The segment left the square: the line continues elsewhere.
10342
+ if (b !== line[i + 1]) {
10343
+ parts.push(current);
10344
+ current = [];
10345
+ }
10346
+ }
10347
+ if (current.length > 1)
10348
+ parts.push(current);
10349
+ return parts;
10350
+ }
10351
+ /** Liang–Barsky: the part of the segment a→b inside the square, or undefined. */
10352
+ function clipSegment(a, b, min, max) {
10353
+ const dx = b.x - a.x;
10354
+ const dy = b.y - a.y;
10355
+ let t0 = 0;
10356
+ let t1 = 1;
10357
+ for (const [p, q] of [
10358
+ [-dx, a.x - min],
10359
+ [dx, max - a.x],
10360
+ [-dy, a.y - min],
10361
+ [dy, max - a.y],
10362
+ ]) {
10363
+ if (p === 0) {
10364
+ if (q < 0)
10365
+ return undefined;
10366
+ continue;
10367
+ }
10368
+ const t = q / p;
10369
+ if (p < 0) {
10370
+ if (t > t1)
10371
+ return undefined;
10372
+ if (t > t0)
10373
+ t0 = t;
10374
+ }
10375
+ else {
10376
+ if (t < t0)
10377
+ return undefined;
10378
+ if (t < t1)
10379
+ t1 = t;
10380
+ }
10381
+ }
10382
+ return [
10383
+ t0 === 0 ? a : { x: a.x + dx * t0, y: a.y + dy * t0 },
10384
+ t1 === 1 ? b : { x: a.x + dx * t1, y: a.y + dy * t1 },
10385
+ ];
10386
+ }
10387
+ function clipAgainst(points, inside, intersect) {
10388
+ const result = [];
10389
+ for (let i = 0; i < points.length; i++) {
10390
+ const current = points[i];
10391
+ const previous = points[(i + points.length - 1) % points.length];
10392
+ const currentInside = inside(current);
10393
+ if (currentInside !== inside(previous))
10394
+ result.push(intersect(previous, current));
10395
+ if (currentInside)
10396
+ result.push(current);
10397
+ }
10398
+ return result;
10399
+ }
10400
+ function lerpAtX(a, b, x) {
10401
+ return { x, y: a.y + ((b.y - a.y) * (x - a.x)) / (b.x - a.x) };
10402
+ }
10403
+ function lerpAtY(a, b, y) {
10404
+ return { x: a.x + ((b.x - a.x) * (y - a.y)) / (b.y - a.y), y };
10405
+ }
10406
+
10032
10407
  const TILE_EXTENT = 4096;
10033
10408
  const VTFeatureType = { Unknown: 0, Point: 1, LineString: 2, Polygon: 3 };
10034
10409
  async function loadVectorSource(source, job, layerFeatures) {
@@ -10036,11 +10411,8 @@
10036
10411
  if (!tiles)
10037
10412
  return;
10038
10413
  const { width, height } = job.renderer;
10039
- const { zoom, center } = job.view;
10040
- const { zoomLevel, tileSize, tiles: tileCoordinates, } = calculateTileGrid(width, height, center, zoom, source.maxzoom);
10041
- await Promise.all(tileCoordinates.map(async ({ x, y, offsetX, offsetY }) => {
10042
- const offset = new Point2D(offsetX, offsetY);
10043
- const tile = await getTile(tiles[0], zoomLevel, x, y);
10414
+ await Promise.all(getTileProjections(source, job).map(async ({ x, y, z, project, clipToTile }) => {
10415
+ const tile = await getTile(tiles[0], z, x, y);
10044
10416
  if (!tile)
10045
10417
  return;
10046
10418
  const vectorTile = new VectorTile(new PbfReader(tile.buffer));
@@ -10052,9 +10424,6 @@
10052
10424
  }
10053
10425
  for (let i = 0; i < layer.length; i++) {
10054
10426
  const featureSrc = layer.feature(i);
10055
- const geometry = featureSrc
10056
- .loadGeometry()
10057
- .map((ring) => ring.map((point) => new Point2D(point.x, point.y).scale(tileSize / TILE_EXTENT).translate(offset)));
10058
10427
  let type;
10059
10428
  let list;
10060
10429
  switch (featureSrc.type) {
@@ -10073,6 +10442,22 @@
10073
10442
  list = features.polygons;
10074
10443
  break;
10075
10444
  }
10445
+ let rings = featureSrc.loadGeometry();
10446
+ let outline;
10447
+ // Clip polygons and lines to the tile, like MapLibre's stencil clipping: otherwise
10448
+ // the parts in the tile buffer are drawn twice, which shows when translucent.
10449
+ if (clipToTile && type !== 'Point' && exceedsSquare(rings, 0, TILE_EXTENT)) {
10450
+ if (type === 'Polygon') {
10451
+ outline = project('LineString', clipPolygonOutline(rings, 0, TILE_EXTENT));
10452
+ rings = clipPolygon(rings, 0, TILE_EXTENT);
10453
+ }
10454
+ else {
10455
+ rings = rings.flatMap((line) => clipLine(line, 0, TILE_EXTENT));
10456
+ }
10457
+ }
10458
+ const geometry = project(type, rings);
10459
+ if (geometry.length === 0)
10460
+ continue;
10076
10461
  // Split MultiPoint into individual Point features
10077
10462
  if (type === 'Point' && geometry.length > 1) {
10078
10463
  for (const ring of geometry) {
@@ -10090,6 +10475,7 @@
10090
10475
  const feature = new Feature({
10091
10476
  type,
10092
10477
  geometry,
10478
+ outline,
10093
10479
  id: featureSrc.id,
10094
10480
  properties: featureSrc.properties,
10095
10481
  });
@@ -10100,9 +10486,41 @@
10100
10486
  }
10101
10487
  }));
10102
10488
  }
10489
+ function getTileProjections(source, job) {
10490
+ const { width, height } = job.renderer;
10491
+ const { zoom, center } = job.view;
10492
+ const projection = job.projection;
10493
+ if (projection?.isGlobe) {
10494
+ // Never zoom level 0 on the globe: the left and right edge of its single tile coincide
10495
+ // (the antimeridian), which the horizon clipping cannot tell apart.
10496
+ const z = Math.max(1, Math.min(Math.floor(zoom), source.maxzoom ?? Infinity));
10497
+ const scale = 1 / (TILE_EXTENT * 2 ** z);
10498
+ return projection.coveringTiles(z).map(({ x, y }) => ({
10499
+ x,
10500
+ y,
10501
+ z,
10502
+ clipToTile: projection.isTileFullyVisible({ x, y, z }),
10503
+ project: (type, rings) => projection.projectGeometry(type, rings.map((ring) => ring.map((p) => [
10504
+ (x * TILE_EXTENT + p.x) * scale,
10505
+ (y * TILE_EXTENT + p.y) * scale,
10506
+ ]))),
10507
+ }));
10508
+ }
10509
+ const { zoomLevel, tileSize, tiles } = calculateTileGrid(width, height, center, zoom, source.maxzoom);
10510
+ return tiles.map(({ x, y, offsetX, offsetY }) => {
10511
+ const offset = new Point2D(offsetX, offsetY);
10512
+ return {
10513
+ x,
10514
+ y,
10515
+ z: zoomLevel,
10516
+ clipToTile: true,
10517
+ project: (_type, rings) => rings.map((ring) => ring.map((point) => new Point2D(point.x, point.y).scale(tileSize / TILE_EXTENT).translate(offset))),
10518
+ };
10519
+ });
10520
+ }
10103
10521
 
10104
10522
  function loadGeoJSONSource(options) {
10105
- const { sourceName, data, width, height, zoom, center, layerFeatures } = options;
10523
+ const { sourceName, data, width, height, zoom, center, layerFeatures, projection } = options;
10106
10524
  const existing = layerFeatures.get(sourceName);
10107
10525
  const features = existing ?? {
10108
10526
  points: [],
@@ -10115,9 +10533,17 @@
10115
10533
  layerFeatures.set(sourceName, features);
10116
10534
  const worldSize = 512 * 2 ** zoom;
10117
10535
  const centerMercator = new Point2D(center[0], center[1]).getProject2Pixel();
10118
- function projectCoord(coord) {
10536
+ function toMercator(coord) {
10119
10537
  const mercator = new Point2D(coord[0], coord[1]).getProject2Pixel();
10120
- return new Point2D((mercator.x - centerMercator.x) * worldSize + width / 2, (mercator.y - centerMercator.y) * worldSize + height / 2);
10538
+ return [mercator.x, mercator.y];
10539
+ }
10540
+ function project(type, rings) {
10541
+ if (projection?.isGlobe) {
10542
+ if (type === 'Polygon')
10543
+ rings = orientRings(rings);
10544
+ return projection.projectGeometry(type, rings);
10545
+ }
10546
+ return rings.map((ring) => ring.map(([x, y]) => new Point2D((x - centerMercator.x) * worldSize + width / 2, (y - centerMercator.y) * worldSize + height / 2)));
10121
10547
  }
10122
10548
  function makeFeature(type, geometry, id, properties) {
10123
10549
  const feature = new Feature({ type, geometry, id, properties });
@@ -10128,7 +10554,10 @@
10128
10554
  function extractPoints(geometry) {
10129
10555
  return geometry.flatMap((ring) => ring.map((p) => [p]));
10130
10556
  }
10131
- function addFeature(type, geometry, id, properties) {
10557
+ function addFeature(type, rings, id, properties) {
10558
+ const geometry = project(type, rings);
10559
+ if (geometry.length === 0)
10560
+ return;
10132
10561
  switch (type) {
10133
10562
  case 'Point': {
10134
10563
  const f = makeFeature('Point', geometry, id, properties);
@@ -10171,23 +10600,23 @@
10171
10600
  function processGeometry(geom, id, properties) {
10172
10601
  switch (geom.type) {
10173
10602
  case 'Point':
10174
- addFeature('Point', [[projectCoord(geom.coordinates)]], id, properties);
10603
+ addFeature('Point', [[toMercator(geom.coordinates)]], id, properties);
10175
10604
  break;
10176
10605
  case 'MultiPoint':
10177
- addFeature('Point', geom.coordinates.map((c) => [projectCoord(c)]), id, properties);
10606
+ addFeature('Point', geom.coordinates.map((c) => [toMercator(c)]), id, properties);
10178
10607
  break;
10179
10608
  case 'LineString':
10180
- addFeature('LineString', [geom.coordinates.map((c) => projectCoord(c))], id, properties);
10609
+ addFeature('LineString', [geom.coordinates.map((c) => toMercator(c))], id, properties);
10181
10610
  break;
10182
10611
  case 'MultiLineString':
10183
- addFeature('LineString', geom.coordinates.map((line) => line.map((c) => projectCoord(c))), id, properties);
10612
+ addFeature('LineString', geom.coordinates.map((line) => line.map((c) => toMercator(c))), id, properties);
10184
10613
  break;
10185
10614
  case 'Polygon':
10186
- addFeature('Polygon', geom.coordinates.map((ring) => ring.map((c) => projectCoord(c))), id, properties);
10615
+ addFeature('Polygon', geom.coordinates.map((ring) => ring.map((c) => toMercator(c))), id, properties);
10187
10616
  break;
10188
10617
  case 'MultiPolygon':
10189
10618
  for (const polygon of geom.coordinates) {
10190
- addFeature('Polygon', polygon.map((ring) => ring.map((c) => projectCoord(c))), id, properties);
10619
+ addFeature('Polygon', polygon.map((ring) => ring.map((c) => toMercator(c))), id, properties);
10191
10620
  }
10192
10621
  break;
10193
10622
  case 'GeometryCollection':
@@ -10211,6 +10640,22 @@
10211
10640
  break;
10212
10641
  }
10213
10642
  }
10643
+ /**
10644
+ * Orients polygon rings like vector tiles do (in mercator space, y pointing south: the
10645
+ * exterior ring clockwise, holes counter-clockwise), as the globe clipping relies on it.
10646
+ */
10647
+ function orientRings(rings) {
10648
+ return rings.map((ring, index) => {
10649
+ let area = 0;
10650
+ for (let i = 0; i < ring.length; i++) {
10651
+ const [x0, y0] = ring[i];
10652
+ const [x1, y1] = ring[(i + 1) % ring.length];
10653
+ area += x0 * y1 - x1 * y0;
10654
+ }
10655
+ const isExterior = index === 0;
10656
+ return area > 0 === isExterior ? ring : [...ring].reverse();
10657
+ });
10658
+ }
10214
10659
 
10215
10660
  /**
10216
10661
  * Base64-encode an ArrayBuffer (standard alphabet, with padding).
@@ -10248,6 +10693,23 @@
10248
10693
  throw Error(`Invalid raster source "${sourceName}": expected type "raster" with a "tiles" array`);
10249
10694
  }
10250
10695
  const sourceUrl = source.tiles[0];
10696
+ const projection = job.projection;
10697
+ if (projection?.isGlobe) {
10698
+ // Never zoom level 0 on the globe: the left and right edge of its single tile coincide
10699
+ // (the antimeridian), which the horizon clipping cannot tell apart.
10700
+ const z = Math.max(1, Math.min(Math.floor(zoom), source.maxzoom ?? Infinity));
10701
+ const globeTiles = await Promise.all(projection.coveringTiles(z).map(async (id) => {
10702
+ const tile = await getTile(sourceUrl, id.z, id.x, id.y);
10703
+ if (!tile)
10704
+ return null;
10705
+ const triangles = projection.rasterTriangles(id);
10706
+ if (triangles.length === 0)
10707
+ return null;
10708
+ const dataUri = `data:${tile.contentType};base64,${arrayBufferToBase64(tile.buffer)}`;
10709
+ return { x: 0, y: 0, width: 1, height: 1, dataUri, triangles };
10710
+ }));
10711
+ return globeTiles.filter((tile) => tile !== null);
10712
+ }
10251
10713
  const { zoomLevel, tileSize, tiles } = calculateTileGrid(width, height, center, zoom, source.maxzoom);
10252
10714
  const rasterTiles = await Promise.all(tiles.map(async ({ x, y, offsetX, offsetY }) => {
10253
10715
  const tile = await getTile(sourceUrl, zoomLevel, x, y);
@@ -10351,6 +10813,7 @@
10351
10813
  zoom,
10352
10814
  center,
10353
10815
  layerFeatures,
10816
+ projection: job.projection,
10354
10817
  });
10355
10818
  }
10356
10819
  break;
@@ -10482,6 +10945,594 @@
10482
10945
  return layers.map(createStyleLayer);
10483
10946
  }
10484
10947
 
10948
+ /**
10949
+ * Map projection: web mercator, MapLibre's globe ("vertical-perspective") or a blend of both.
10950
+ *
10951
+ * The math follows MapLibre GL JS (`vertical_perspective_transform.ts` and the
10952
+ * `_projection_globe.vertex.glsl` shader) for a camera without pitch, bearing or padding:
10953
+ * the globe is a sphere of radius `worldSize / (2π·cos(centerLat))` (so the map center has
10954
+ * the same scale as in mercator), viewed by a perspective camera `1.5 · height` pixels above
10955
+ * the map center. The globe → mercator transition blends the homogeneous clip-space positions
10956
+ * of both projections (x, y and w) *before* the perspective divide, exactly like the shader.
10957
+ *
10958
+ * All inputs are mercator world coordinates in the range 0..1 (`mx` east, `my` south).
10959
+ */
10960
+ /** MapLibre's default vertical field of view (`TransformHelper._fovInRadians`). */
10961
+ const FOV = 0.6435011087932844;
10962
+ /** What MapLibre expands `projection: { type: 'globe' }` into (`projection_factory.ts`). */
10963
+ const GLOBE_PRESET = [
10964
+ 'interpolate',
10965
+ ['linear'],
10966
+ ['zoom'],
10967
+ 11,
10968
+ 'vertical-perspective',
10969
+ 12,
10970
+ 'mercator',
10971
+ ];
10972
+ /** Below this globeness the far side of the globe is not clipped (MapLibre's `z_globeness_threshold`). */
10973
+ const CLIP_THRESHOLD = 0.2;
10974
+ /** Maximum distance (pixels) a straight segment may deviate from the curved globe surface. */
10975
+ const MAX_CURVE_ERROR_PX = 0.25;
10976
+ /** The same for the affine-mapped triangles of raster tiles (a coarser mesh keeps the SVG small). */
10977
+ const MAX_RASTER_ERROR_PX = 1;
10978
+ /** Upper bound for the number of cells per raster tile side. */
10979
+ const MAX_RASTER_CELLS = 16;
10980
+ /** Angle to go clockwise on screen (decreasing angle in the y-up view frame) from a0 to a1, in [0, 2π). */
10981
+ function clockwiseDelta(a0, a1) {
10982
+ const delta = (a0 - a1) % (2 * Math.PI);
10983
+ return delta < 0 ? delta + 2 * Math.PI : delta;
10984
+ }
10985
+ /**
10986
+ * Returns how "globe-like" the projection is at the given zoom, matching MapLibre's
10987
+ * `GlobeProjection.transitionState`: 0 = mercator, 1 = globe, in between = transition.
10988
+ */
10989
+ function getGlobeness(projection, zoom) {
10990
+ return evaluateGlobeness(projection?.type ?? 'mercator', zoom);
10991
+ }
10992
+ function evaluateGlobeness(value, zoom) {
10993
+ if (typeof value === 'string') {
10994
+ switch (value) {
10995
+ case 'globe':
10996
+ return evaluateGlobeness(GLOBE_PRESET, zoom);
10997
+ case 'vertical-perspective':
10998
+ return 1;
10999
+ default:
11000
+ return 0;
11001
+ }
11002
+ }
11003
+ if (!Array.isArray(value))
11004
+ return 0;
11005
+ const [op, ...args] = value;
11006
+ if (op === 'literal')
11007
+ return evaluateGlobeness(args[0], zoom);
11008
+ if (op === 'step') {
11009
+ // ["step", ["zoom"], output0, stop1, output1, ...]
11010
+ let result = args[1];
11011
+ for (let i = 2; i + 1 < args.length; i += 2) {
11012
+ if (zoom >= args[i])
11013
+ result = args[i + 1];
11014
+ }
11015
+ return evaluateGlobeness(result, zoom);
11016
+ }
11017
+ if (op === 'interpolate') {
11018
+ // ["interpolate", ["linear"] | ["exponential", base], ["zoom"], stop0, output0, ...]
11019
+ const interpolation = args[0];
11020
+ const base = interpolation[0] === 'exponential' ? interpolation[1] : 1;
11021
+ const stops = [];
11022
+ for (let i = 2; i + 1 < args.length; i += 2)
11023
+ stops.push([args[i], args[i + 1]]);
11024
+ if (stops.length === 0)
11025
+ return 0;
11026
+ if (zoom <= stops[0][0])
11027
+ return evaluateGlobeness(stops[0][1], zoom);
11028
+ for (let i = 1; i < stops.length; i++) {
11029
+ const [z1, out1] = stops[i];
11030
+ if (zoom > z1)
11031
+ continue;
11032
+ const [z0, out0] = stops[i - 1];
11033
+ const t = interpolationFactor(zoom, z0, z1, base);
11034
+ const g0 = evaluateGlobeness(out0, zoom);
11035
+ const g1 = evaluateGlobeness(out1, zoom);
11036
+ return g0 + (g1 - g0) * t;
11037
+ }
11038
+ return evaluateGlobeness(stops[stops.length - 1][1], zoom);
11039
+ }
11040
+ // A projection transition literal: [from, to, transition]
11041
+ if (value.length === 3 && typeof value[2] === 'number') {
11042
+ const g0 = evaluateGlobeness(value[0], zoom);
11043
+ const g1 = evaluateGlobeness(value[1], zoom);
11044
+ return g0 + (g1 - g0) * value[2];
11045
+ }
11046
+ return 0;
11047
+ }
11048
+ function interpolationFactor(input, lower, upper, base) {
11049
+ const difference = upper - lower;
11050
+ if (difference === 0)
11051
+ return 0;
11052
+ const progress = input - lower;
11053
+ if (base === 1)
11054
+ return progress / difference;
11055
+ return (Math.pow(base, progress) - 1) / (Math.pow(base, difference) - 1);
11056
+ }
11057
+ class Projection {
11058
+ width;
11059
+ height;
11060
+ zoom;
11061
+ /** 0 = mercator, 1 = globe, in between = transition (see {@link getGlobeness}). */
11062
+ globeness;
11063
+ /** Size of the whole world in pixels at the current zoom (mercator). */
11064
+ worldSize;
11065
+ /**
11066
+ * Maximum length (in mercator units) of a straight segment before it has to be
11067
+ * subdivided to follow the curvature of the globe.
11068
+ */
11069
+ maxSegmentLength;
11070
+ /** Whether geometry on the far side of the globe has to be removed. */
11071
+ clipsHorizon;
11072
+ #centerX;
11073
+ #centerY;
11074
+ #centerLng;
11075
+ #sinLat;
11076
+ #cosLat;
11077
+ /** Globe radius in pixels. */
11078
+ #radius;
11079
+ /** Distance of the camera to the map center in pixels. */
11080
+ #cameraDistance;
11081
+ /** Points of the unit sphere with a view-space z below this are behind the horizon. */
11082
+ #horizonZ;
11083
+ constructor(opt) {
11084
+ this.width = opt.width;
11085
+ this.height = opt.height;
11086
+ this.zoom = opt.zoom;
11087
+ this.globeness = Math.min(1, Math.max(0, opt.globeness ?? 0));
11088
+ this.worldSize = 512 * 2 ** opt.zoom;
11089
+ const center = new Point2D(opt.center[0], opt.center[1]).getProject2Pixel();
11090
+ this.#centerX = center.x;
11091
+ this.#centerY = center.y;
11092
+ const lat = (opt.center[1] * Math.PI) / 180;
11093
+ this.#centerLng = (opt.center[0] * Math.PI) / 180;
11094
+ this.#sinLat = Math.sin(lat);
11095
+ this.#cosLat = Math.cos(lat);
11096
+ this.#radius = this.worldSize / (2 * Math.PI * this.#cosLat);
11097
+ this.#cameraDistance = this.height / 2 / Math.tan(FOV / 2);
11098
+ this.#horizonZ = this.#radius / (this.#radius + this.#cameraDistance);
11099
+ this.clipsHorizon = this.globeness > CLIP_THRESHOLD;
11100
+ // A chord spanning the angle θ deviates R·θ²/8 from the sphere.
11101
+ const maxAngle = Math.sqrt((8 * MAX_CURVE_ERROR_PX) / this.#radius);
11102
+ this.maxSegmentLength = maxAngle / (2 * Math.PI);
11103
+ }
11104
+ static fromStyle(opt) {
11105
+ return new Projection({ ...opt, globeness: getGlobeness(opt.projection, opt.zoom) });
11106
+ }
11107
+ /** True when the globe projection is (at least partially) in effect. */
11108
+ get isGlobe() {
11109
+ return this.globeness > 0;
11110
+ }
11111
+ /**
11112
+ * The silhouette of the globe on screen, if the map has to be clipped to it.
11113
+ * (During the globe → mercator transition the silhouette is far outside the screen.)
11114
+ */
11115
+ get clipCircle() {
11116
+ if (!this.clipsHorizon)
11117
+ return undefined;
11118
+ const r = this.#radius;
11119
+ const d = this.#cameraDistance;
11120
+ const radius = (d * r) / Math.sqrt((d + r) * (d + r) - r * r);
11121
+ return { x: this.width / 2, y: this.height / 2, radius };
11122
+ }
11123
+ /** Mercator world coordinates → unit sphere in the view frame (x right, y up, z towards the camera). */
11124
+ toSphere(mx, my) {
11125
+ const dLng = mx * 2 * Math.PI - Math.PI - this.#centerLng;
11126
+ const lat = 2 * Math.atan(Math.exp(Math.PI - my * 2 * Math.PI)) - Math.PI / 2;
11127
+ const cosLat = Math.cos(lat);
11128
+ const x = Math.sin(dLng) * cosLat;
11129
+ const y = Math.sin(lat);
11130
+ const z = Math.cos(dLng) * cosLat;
11131
+ return [x, y * this.#cosLat - z * this.#sinLat, y * this.#sinLat + z * this.#cosLat];
11132
+ }
11133
+ /** Inverse of {@link toSphere}. */
11134
+ fromSphere(v) {
11135
+ const y = v[1] * this.#cosLat + v[2] * this.#sinLat;
11136
+ const z = -v[1] * this.#sinLat + v[2] * this.#cosLat;
11137
+ const lat = Math.asin(Math.max(-1, Math.min(1, y)));
11138
+ const lng = Math.atan2(v[0], z) + this.#centerLng;
11139
+ const mx = (lng + Math.PI) / (2 * Math.PI);
11140
+ const my = (Math.PI - Math.log(Math.tan(Math.PI / 4 + lat / 2))) / (2 * Math.PI);
11141
+ return [mx - Math.floor(mx), my];
11142
+ }
11143
+ isVisible(v) {
11144
+ return !this.clipsHorizon || v[2] >= this.#horizonZ;
11145
+ }
11146
+ /** Screen position of a mercator point. */
11147
+ project(mx, my, v) {
11148
+ // Flat (mercator) position, relative to the screen center. Use the world copy
11149
+ // nearest to the center.
11150
+ let dx = mx - this.#centerX;
11151
+ dx -= Math.round(dx);
11152
+ const flatX = dx * this.worldSize;
11153
+ const flatY = (my - this.#centerY) * this.worldSize;
11154
+ const t = this.globeness;
11155
+ if (t === 0)
11156
+ return new Point2D(flatX + this.width / 2, flatY + this.height / 2);
11157
+ v ??= this.toSphere(mx, my);
11158
+ const d = this.#cameraDistance;
11159
+ const r = this.#radius;
11160
+ // Homogeneous coordinates (x, y, w) of the globe position; the flat one is (d·x, d·y, d).
11161
+ const globeX = d * r * v[0];
11162
+ const globeY = -d * r * v[1];
11163
+ const globeW = d + r * (1 - v[2]);
11164
+ const w = d + (globeW - d) * t;
11165
+ return new Point2D((d * flatX + (globeX - d * flatX) * t) / w + this.width / 2, (d * flatY + (globeY - d * flatY) * t) / w + this.height / 2);
11166
+ }
11167
+ /**
11168
+ * Projects feature geometry given in mercator world coordinates to screen pixels.
11169
+ * On the globe, segments are subdivided to follow the curvature, lines are cut at
11170
+ * the horizon and polygons are clipped to the visible hemisphere (the clipped part of
11171
+ * the boundary follows the globe's silhouette).
11172
+ */
11173
+ projectGeometry(type, rings) {
11174
+ if (!this.isGlobe)
11175
+ return rings.map((ring) => ring.map(([mx, my]) => this.project(mx, my)));
11176
+ switch (type) {
11177
+ case 'Point':
11178
+ return rings
11179
+ .map((ring) => ring.flatMap(([mx, my]) => {
11180
+ const v = this.toSphere(mx, my);
11181
+ return this.isVisible(v) ? [this.project(mx, my, v)] : [];
11182
+ }))
11183
+ .filter((ring) => ring.length > 0);
11184
+ case 'LineString':
11185
+ return rings.flatMap((ring) => this.#clipLine(this.#densify(ring, false)));
11186
+ case 'Polygon':
11187
+ return this.#clipPolygon(rings.map((ring) => this.#densify(ring, true)));
11188
+ }
11189
+ }
11190
+ /** Subdivides long segments (in mercator space) and lifts the points onto the sphere. */
11191
+ #densify(ring, closed) {
11192
+ const maxLength = this.maxSegmentLength;
11193
+ const result = [];
11194
+ const count = ring.length;
11195
+ const segments = closed ? count : count - 1;
11196
+ for (let i = 0; i < count; i++) {
11197
+ const [x0, y0] = ring[i];
11198
+ result.push({ mx: x0, my: y0, v: this.toSphere(x0, y0) });
11199
+ if (i >= segments)
11200
+ continue;
11201
+ const [x1, y1] = ring[(i + 1) % count];
11202
+ const steps = Math.ceil(Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0)) / maxLength);
11203
+ for (let s = 1; s < steps; s++) {
11204
+ const mx = x0 + ((x1 - x0) * s) / steps;
11205
+ const my = y0 + ((y1 - y0) * s) / steps;
11206
+ result.push({ mx, my, v: this.toSphere(mx, my) });
11207
+ }
11208
+ }
11209
+ return result;
11210
+ }
11211
+ /** Point on the segment a→b where it crosses the horizon plane. */
11212
+ #horizonCrossing(a, b) {
11213
+ const s = (a.v[2] - this.#horizonZ) / (a.v[2] - b.v[2]);
11214
+ const mx = a.mx + (b.mx - a.mx) * s;
11215
+ const my = a.my + (b.my - a.my) * s;
11216
+ return { mx, my, v: this.toSphere(mx, my) };
11217
+ }
11218
+ /** Splits a line into its visible parts. */
11219
+ #clipLine(points) {
11220
+ if (!this.clipsHorizon)
11221
+ return [points.map((p) => this.project(p.mx, p.my, p.v))];
11222
+ const parts = [];
11223
+ let current = [];
11224
+ for (let i = 0; i < points.length; i++) {
11225
+ const p = points[i];
11226
+ const visible = this.isVisible(p.v);
11227
+ const prev = i > 0 ? points[i - 1] : undefined;
11228
+ if (prev && this.isVisible(prev.v) !== visible) {
11229
+ const c = this.#horizonCrossing(prev, p);
11230
+ current.push(this.project(c.mx, c.my, c.v));
11231
+ if (!visible) {
11232
+ parts.push(current);
11233
+ current = [];
11234
+ }
11235
+ }
11236
+ if (visible)
11237
+ current.push(this.project(p.mx, p.my, p.v));
11238
+ }
11239
+ if (current.length > 0)
11240
+ parts.push(current);
11241
+ return parts.filter((part) => part.length > 1);
11242
+ }
11243
+ /**
11244
+ * Clips the rings of a polygon to the visible hemisphere.
11245
+ *
11246
+ * Every ring crossing the horizon is cut into its visible chains, each running from an
11247
+ * entry point to an exit point on the horizon. The chains are then reconnected along the
11248
+ * silhouette: from an exit point, the silhouette is followed to the nearest entry point
11249
+ * (of any chain of any ring). Rings follow the vector tile winding (exterior rings
11250
+ * clockwise on screen, holes counter-clockwise), which keeps the filled area on the right
11251
+ * of every ring, so the silhouette is always walked clockwise.
11252
+ */
11253
+ #clipPolygon(rings) {
11254
+ const project = (p) => this.project(p.mx, p.my, p.v);
11255
+ if (!this.clipsHorizon)
11256
+ return rings.map((ring) => ring.map(project));
11257
+ const result = [];
11258
+ const chains = [];
11259
+ const angleOf = (p) => Math.atan2(p.v[1], p.v[0]);
11260
+ for (const ring of rings) {
11261
+ const count = ring.length;
11262
+ const visible = ring.map((p) => this.isVisible(p.v));
11263
+ // Start walking at an entry point, so every chain is complete.
11264
+ let start = -1;
11265
+ for (let i = 0; i < count; i++) {
11266
+ if (visible[i] && !visible[(i + count - 1) % count]) {
11267
+ start = i;
11268
+ break;
11269
+ }
11270
+ }
11271
+ if (start < 0) {
11272
+ // No crossing: the ring is either completely visible or completely hidden.
11273
+ if (visible[0])
11274
+ result.push(ring.map(project));
11275
+ continue;
11276
+ }
11277
+ let chain;
11278
+ for (let k = 0; k < count; k++) {
11279
+ const i = (start + k) % count;
11280
+ const p = ring[i];
11281
+ const prev = ring[(i + count - 1) % count];
11282
+ if (visible[i]) {
11283
+ if (!chain) {
11284
+ const c = this.#horizonCrossing(prev, p);
11285
+ chain = { entry: angleOf(c), exit: 0, points: [project(c)] };
11286
+ }
11287
+ chain.points.push(project(p));
11288
+ }
11289
+ else if (chain) {
11290
+ const c = this.#horizonCrossing(prev, p);
11291
+ chain.points.push(project(c));
11292
+ chain.exit = angleOf(c);
11293
+ chains.push(chain);
11294
+ chain = undefined;
11295
+ }
11296
+ }
11297
+ }
11298
+ // Reconnect the chains along the silhouette.
11299
+ const used = new Set();
11300
+ for (const first of chains) {
11301
+ if (used.has(first))
11302
+ continue;
11303
+ const ring = [];
11304
+ let chain = first;
11305
+ for (;;) {
11306
+ used.add(chain);
11307
+ ring.push(...chain.points);
11308
+ let next = first;
11309
+ let bestDelta = clockwiseDelta(chain.exit, first.entry);
11310
+ for (const candidate of chains) {
11311
+ if (used.has(candidate))
11312
+ continue;
11313
+ const delta = clockwiseDelta(chain.exit, candidate.entry);
11314
+ if (delta < bestDelta) {
11315
+ bestDelta = delta;
11316
+ next = candidate;
11317
+ }
11318
+ }
11319
+ this.#pushArc(ring, chain.exit, next.entry);
11320
+ if (next === first)
11321
+ break;
11322
+ chain = next;
11323
+ }
11324
+ result.push(ring);
11325
+ }
11326
+ return result;
11327
+ }
11328
+ /**
11329
+ * Appends points along the horizon circle, clockwise on screen (decreasing angle in the
11330
+ * y-up view frame) from angle a0 to a1.
11331
+ */
11332
+ #pushArc(result, a0, a1) {
11333
+ const delta = clockwiseDelta(a0, a1);
11334
+ const steps = Math.ceil(delta / (Math.PI / 90));
11335
+ const h = this.#horizonZ;
11336
+ const rho = Math.sqrt(1 - h * h);
11337
+ for (let s = 1; s < steps; s++) {
11338
+ const a = a0 - (delta * s) / steps;
11339
+ const v = [rho * Math.cos(a), rho * Math.sin(a), h];
11340
+ const [mx, my] = this.fromSphere(v);
11341
+ result.push(this.project(mx, my, v));
11342
+ }
11343
+ }
11344
+ /**
11345
+ * Splits a raster tile into a mesh of triangles, each small enough to be drawn with an
11346
+ * affine transform (see {@link RasterTriangle}). An affine transform is fully determined
11347
+ * by three points, so every triangle meets its projected corners exactly and neighbouring
11348
+ * triangles share their edges. Cells on the far side of the globe are dropped.
11349
+ */
11350
+ rasterTriangles(tile) {
11351
+ const tileSize = 1 / 2 ** tile.z;
11352
+ const maxLength = this.maxSegmentLength * Math.sqrt(MAX_RASTER_ERROR_PX / MAX_CURVE_ERROR_PX);
11353
+ const n = Math.max(1, Math.min(MAX_RASTER_CELLS, Math.ceil(tileSize / maxLength)));
11354
+ // Project the (n+1)² grid corners once.
11355
+ const corners = [];
11356
+ for (let j = 0; j <= n; j++) {
11357
+ for (let i = 0; i <= n; i++) {
11358
+ const mx = (tile.x + i / n) * tileSize;
11359
+ const my = (tile.y + j / n) * tileSize;
11360
+ let v = this.toSphere(mx, my);
11361
+ if (this.isVisible(v)) {
11362
+ corners.push(this.project(mx, my, v));
11363
+ }
11364
+ else {
11365
+ // Pull the corner onto the horizon, so cells at the silhouette end there.
11366
+ if (this.#isBehind(v)) {
11367
+ corners.push(undefined);
11368
+ continue;
11369
+ }
11370
+ v = this.#clampToHorizon(v);
11371
+ const [cx, cy] = this.fromSphere(v);
11372
+ corners.push(this.project(cx, cy, v));
11373
+ }
11374
+ }
11375
+ }
11376
+ const triangles = [];
11377
+ for (let j = 0; j < n; j++) {
11378
+ for (let i = 0; i < n; i++) {
11379
+ const p00 = corners[j * (n + 1) + i];
11380
+ const p10 = corners[j * (n + 1) + i + 1];
11381
+ const p01 = corners[(j + 1) * (n + 1) + i];
11382
+ const p11 = corners[(j + 1) * (n + 1) + i + 1];
11383
+ if (!p00 || !p10 || !p01 || !p11)
11384
+ continue;
11385
+ const minX = Math.min(p00.x, p10.x, p01.x, p11.x);
11386
+ const maxX = Math.max(p00.x, p10.x, p01.x, p11.x);
11387
+ const minY = Math.min(p00.y, p10.y, p01.y, p11.y);
11388
+ const maxY = Math.max(p00.y, p10.y, p01.y, p11.y);
11389
+ if (maxX < 0 || minX > this.width || maxY < 0 || minY > this.height)
11390
+ continue;
11391
+ if (!this.#isCellVisible(tile, n, i, j))
11392
+ continue;
11393
+ const u0 = i / n;
11394
+ const v0 = j / n;
11395
+ const u1 = (i + 1) / n;
11396
+ const v1 = (j + 1) / n;
11397
+ triangles.push({
11398
+ source: [
11399
+ [u0, v0],
11400
+ [u1, v0],
11401
+ [u1, v1],
11402
+ ],
11403
+ target: [
11404
+ [p00.x, p00.y],
11405
+ [p10.x, p10.y],
11406
+ [p11.x, p11.y],
11407
+ ],
11408
+ }, {
11409
+ source: [
11410
+ [u0, v0],
11411
+ [u1, v1],
11412
+ [u0, v1],
11413
+ ],
11414
+ target: [
11415
+ [p00.x, p00.y],
11416
+ [p11.x, p11.y],
11417
+ [p01.x, p01.y],
11418
+ ],
11419
+ });
11420
+ }
11421
+ }
11422
+ return triangles;
11423
+ }
11424
+ /** Whether any part of a raster cell is on the visible side of the globe. */
11425
+ #isCellVisible(tile, n, i, j) {
11426
+ if (!this.clipsHorizon)
11427
+ return true;
11428
+ const tileSize = 1 / 2 ** tile.z;
11429
+ for (let dj = 0; dj <= 2; dj++) {
11430
+ for (let di = 0; di <= 2; di++) {
11431
+ const mx = (tile.x + (i + di / 2) / n) * tileSize;
11432
+ const my = (tile.y + (j + dj / 2) / n) * tileSize;
11433
+ if (this.isVisible(this.toSphere(mx, my)))
11434
+ return true;
11435
+ }
11436
+ }
11437
+ return false;
11438
+ }
11439
+ /** Moves a point on the far side of the globe onto the horizon (keeping its direction on screen). */
11440
+ #clampToHorizon(v) {
11441
+ const h = this.#horizonZ;
11442
+ const rho = Math.sqrt(1 - h * h);
11443
+ const len = Math.hypot(v[0], v[1]);
11444
+ if (len < 1e-12)
11445
+ return [rho, 0, h];
11446
+ return [(v[0] / len) * rho, (v[1] / len) * rho, h];
11447
+ }
11448
+ /** Points almost opposite the view direction have no meaningful place on the horizon. */
11449
+ #isBehind(v) {
11450
+ return Math.hypot(v[0], v[1]) < 1e-9;
11451
+ }
11452
+ /**
11453
+ * Tiles at zoom level `z` that are visible on screen, found by walking down the tile
11454
+ * pyramid and projecting a grid of sample points of each tile.
11455
+ */
11456
+ coveringTiles(z) {
11457
+ const result = [];
11458
+ const visit = (tz, tx, ty) => {
11459
+ if (tz >= 2 && !this.#isTileVisible(tz, tx, ty))
11460
+ return;
11461
+ if (tz === z) {
11462
+ result.push({ x: tx, y: ty, z: tz });
11463
+ return;
11464
+ }
11465
+ for (let dy = 0; dy < 2; dy++) {
11466
+ for (let dx = 0; dx < 2; dx++)
11467
+ visit(tz + 1, tx * 2 + dx, ty * 2 + dy);
11468
+ }
11469
+ };
11470
+ visit(0, 0, 0);
11471
+ return result;
11472
+ }
11473
+ /**
11474
+ * Whether a tile, including a margin around it (the tile buffer, as a fraction of the tile
11475
+ * size), lies completely on the visible side of the globe, so no horizon clipping happens.
11476
+ */
11477
+ isTileFullyVisible(tile, margin = 0.1) {
11478
+ if (!this.clipsHorizon)
11479
+ return true;
11480
+ const size = 1 / 2 ** tile.z;
11481
+ const samples = 8;
11482
+ for (let j = 0; j <= samples; j++) {
11483
+ for (let i = 0; i <= samples; i++) {
11484
+ if (i !== 0 && i !== samples && j !== 0 && j !== samples)
11485
+ continue; // the border only
11486
+ const mx = (tile.x - margin + ((1 + 2 * margin) * i) / samples) * size;
11487
+ const my = (tile.y - margin + ((1 + 2 * margin) * j) / samples) * size;
11488
+ if (!this.isVisible(this.toSphere(mx, my)))
11489
+ return false;
11490
+ }
11491
+ }
11492
+ return true;
11493
+ }
11494
+ #isTileVisible(z, x, y) {
11495
+ const samples = 6;
11496
+ const size = 1 / 2 ** z;
11497
+ let minX = Infinity;
11498
+ let minY = Infinity;
11499
+ let maxX = -Infinity;
11500
+ let maxY = -Infinity;
11501
+ const addSample = (mx, my) => {
11502
+ const v = this.toSphere(mx, my);
11503
+ if (!this.isVisible(v))
11504
+ return;
11505
+ const p = this.project(mx, my, v);
11506
+ if (p.x < minX)
11507
+ minX = p.x;
11508
+ if (p.y < minY)
11509
+ minY = p.y;
11510
+ if (p.x > maxX)
11511
+ maxX = p.x;
11512
+ if (p.y > maxY)
11513
+ maxY = p.y;
11514
+ };
11515
+ for (let j = 0; j <= samples; j++) {
11516
+ for (let i = 0; i <= samples; i++)
11517
+ addSample((x + i / samples) * size, (y + j / samples) * size);
11518
+ }
11519
+ // Zoomed in, the visible part of the globe can be much smaller than the sample spacing.
11520
+ // So also sample the point of the tile closest to the map center.
11521
+ const x0 = x * size;
11522
+ const centerX = this.#centerX - Math.round(this.#centerX - (x0 + size / 2));
11523
+ addSample(clamp(centerX, x0, x0 + size), clamp(this.#centerY, y * size, (y + 1) * size));
11524
+ // A generous margin: the samples may miss the part of the tile at the silhouette.
11525
+ const margin = Math.max(maxX - minX, maxY - minY) / samples + 1;
11526
+ return (minX - margin <= this.width &&
11527
+ maxX + margin >= 0 &&
11528
+ minY - margin <= this.height &&
11529
+ maxY + margin >= 0);
11530
+ }
11531
+ }
11532
+ function clamp(value, min, max) {
11533
+ return Math.min(max, Math.max(min, value));
11534
+ }
11535
+
10485
11536
  function resolveTokens(text, properties) {
10486
11537
  return text.replace(/\{([^}]+)\}/g, (_, key) => {
10487
11538
  const value = properties[key];
@@ -10495,6 +11546,16 @@
10495
11546
  });
10496
11547
  }
10497
11548
  async function renderMap(job) {
11549
+ job.projection ??= Projection.fromStyle({
11550
+ width: job.renderer.width,
11551
+ height: job.renderer.height,
11552
+ center: job.view.center,
11553
+ zoom: job.view.zoom,
11554
+ projection: job.style.projection,
11555
+ });
11556
+ const clipCircle = job.projection.clipCircle;
11557
+ if (clipCircle)
11558
+ job.renderer.setClipCircle?.(clipCircle);
10498
11559
  await render(job);
10499
11560
  return job.renderer.getString();
10500
11561
  }