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