@versatiles/svg-renderer 1.1.0 → 2.0.0-rc.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 +42 -140
- package/dist/index.d.ts +2665 -4
- package/dist/index.js +2171 -1053
- package/package.json +14 -92
- package/dist/index.cjs +0 -10579
- package/dist/maplibre-svg-export.d.ts +0 -42
- package/dist/maplibre-svg-export.js +0 -11028
- package/dist/maplibre-svg-export.umd.js +0 -11037
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",
|
|
@@ -653,9 +650,9 @@ var v8_default = {
|
|
|
653
650
|
"requires": ["icon-image"],
|
|
654
651
|
"expression": {
|
|
655
652
|
"interpolated": false,
|
|
656
|
-
"parameters": ["zoom"]
|
|
653
|
+
"parameters": ["zoom", "feature"]
|
|
657
654
|
},
|
|
658
|
-
"property-type": "data-
|
|
655
|
+
"property-type": "data-driven"
|
|
659
656
|
},
|
|
660
657
|
"icon-size": {
|
|
661
658
|
"type": "number",
|
|
@@ -1116,6 +1113,31 @@ var v8_default = {
|
|
|
1116
1113
|
},
|
|
1117
1114
|
"property-type": "data-constant"
|
|
1118
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
|
+
},
|
|
1119
1141
|
"visibility": {
|
|
1120
1142
|
"type": "enum",
|
|
1121
1143
|
"values": {
|
|
@@ -1591,7 +1613,6 @@ var v8_default = {
|
|
|
1591
1613
|
"fill-extrusion-height": {
|
|
1592
1614
|
"type": "number",
|
|
1593
1615
|
"default": 0,
|
|
1594
|
-
"minimum": 0,
|
|
1595
1616
|
"units": "meters",
|
|
1596
1617
|
"transition": true,
|
|
1597
1618
|
"expression": {
|
|
@@ -1607,7 +1628,6 @@ var v8_default = {
|
|
|
1607
1628
|
"fill-extrusion-base": {
|
|
1608
1629
|
"type": "number",
|
|
1609
1630
|
"default": 0,
|
|
1610
|
-
"minimum": 0,
|
|
1611
1631
|
"units": "meters",
|
|
1612
1632
|
"transition": true,
|
|
1613
1633
|
"requires": ["fill-extrusion-height"],
|
|
@@ -2642,44 +2662,10 @@ var v8_default = {
|
|
|
2642
2662
|
}
|
|
2643
2663
|
}
|
|
2644
2664
|
};
|
|
2645
|
-
|
|
2665
|
+
|
|
2646
2666
|
//#region src/reference/latest.ts
|
|
2647
2667
|
const latest = v8_default;
|
|
2648
|
-
|
|
2649
|
-
//#region src/expression/parsing_error.ts
|
|
2650
|
-
var ExpressionParsingError = class extends Error {
|
|
2651
|
-
constructor(key, message) {
|
|
2652
|
-
super(message);
|
|
2653
|
-
this.message = message;
|
|
2654
|
-
this.key = key;
|
|
2655
|
-
}
|
|
2656
|
-
};
|
|
2657
|
-
//#endregion
|
|
2658
|
-
//#region src/expression/scope.ts
|
|
2659
|
-
/**
|
|
2660
|
-
* Tracks `let` bindings during expression parsing.
|
|
2661
|
-
* @private
|
|
2662
|
-
*/
|
|
2663
|
-
var Scope = class Scope {
|
|
2664
|
-
constructor(parent, bindings = []) {
|
|
2665
|
-
this.parent = parent;
|
|
2666
|
-
this.bindings = {};
|
|
2667
|
-
for (const [name, expression] of bindings) this.bindings[name] = expression;
|
|
2668
|
-
}
|
|
2669
|
-
concat(bindings) {
|
|
2670
|
-
return new Scope(this, bindings);
|
|
2671
|
-
}
|
|
2672
|
-
get(name) {
|
|
2673
|
-
if (this.bindings[name]) return this.bindings[name];
|
|
2674
|
-
if (this.parent) return this.parent.get(name);
|
|
2675
|
-
throw new Error(`${name} not found in scope.`);
|
|
2676
|
-
}
|
|
2677
|
-
has(name) {
|
|
2678
|
-
if (this.bindings[name]) return true;
|
|
2679
|
-
return this.parent ? this.parent.has(name) : false;
|
|
2680
|
-
}
|
|
2681
|
-
};
|
|
2682
|
-
//#endregion
|
|
2668
|
+
|
|
2683
2669
|
//#region src/expression/types.ts
|
|
2684
2670
|
const NullType = { kind: "null" };
|
|
2685
2671
|
const NumberType = { kind: "number" };
|
|
@@ -2775,7 +2761,7 @@ function verifyType(provided, sample) {
|
|
|
2775
2761
|
if (provided.kind === "array" && sample.kind === "array") return provided.itemType.kind === sample.itemType.kind && typeof provided.N === "number";
|
|
2776
2762
|
return provided.kind === sample.kind;
|
|
2777
2763
|
}
|
|
2778
|
-
|
|
2764
|
+
|
|
2779
2765
|
//#region src/expression/types/color_spaces.ts
|
|
2780
2766
|
const Xn = .96422;
|
|
2781
2767
|
const Yn = 1;
|
|
@@ -2870,7 +2856,7 @@ function hslToRgb([h, s, l, alpha]) {
|
|
|
2870
2856
|
alpha
|
|
2871
2857
|
];
|
|
2872
2858
|
}
|
|
2873
|
-
|
|
2859
|
+
|
|
2874
2860
|
//#region src/util/get_own.ts
|
|
2875
2861
|
const hasOwnProperty = Object.hasOwn || function hasOwnProperty(object, key) {
|
|
2876
2862
|
return Object.prototype.hasOwnProperty.call(object, key);
|
|
@@ -2878,7 +2864,7 @@ const hasOwnProperty = Object.hasOwn || function hasOwnProperty(object, key) {
|
|
|
2878
2864
|
function getOwn(object, key) {
|
|
2879
2865
|
return hasOwnProperty(object, key) ? object[key] : void 0;
|
|
2880
2866
|
}
|
|
2881
|
-
|
|
2867
|
+
|
|
2882
2868
|
//#region src/expression/types/parse_css_color.ts
|
|
2883
2869
|
/**
|
|
2884
2870
|
* CSS color parser compliant with CSS Color 4 Specification.
|
|
@@ -2957,9 +2943,9 @@ function parseCssColor(input) {
|
|
|
2957
2943
|
const maxValue = valFormat === "%%%" ? 100 : valFormat === "" ? 255 : 0;
|
|
2958
2944
|
if (maxValue) {
|
|
2959
2945
|
const rgba = [
|
|
2960
|
-
clamp(+r / maxValue, 0, 1),
|
|
2961
|
-
clamp(+g / maxValue, 0, 1),
|
|
2962
|
-
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),
|
|
2963
2949
|
a ? parseAlpha(+a, ap) : 1
|
|
2964
2950
|
];
|
|
2965
2951
|
if (validateNumbers(rgba)) return rgba;
|
|
@@ -2979,8 +2965,8 @@ function parseCssColor(input) {
|
|
|
2979
2965
|
if (argFormat === " " || argFormat === " /" || argFormat === ",," || argFormat === ",,,") {
|
|
2980
2966
|
const hsla = [
|
|
2981
2967
|
+h,
|
|
2982
|
-
clamp(+s, 0, 100),
|
|
2983
|
-
clamp(+l, 0, 100),
|
|
2968
|
+
clamp$1(+s, 0, 100),
|
|
2969
|
+
clamp$1(+l, 0, 100),
|
|
2984
2970
|
a ? parseAlpha(+a, ap) : 1
|
|
2985
2971
|
];
|
|
2986
2972
|
if (validateNumbers(hsla)) return hslToRgb(hsla);
|
|
@@ -2991,9 +2977,9 @@ function parseHex(hex) {
|
|
|
2991
2977
|
return parseInt(hex.padEnd(2, hex), 16) / 255;
|
|
2992
2978
|
}
|
|
2993
2979
|
function parseAlpha(a, asPercentage) {
|
|
2994
|
-
return clamp(asPercentage ? a / 100 : a, 0, 1);
|
|
2980
|
+
return clamp$1(asPercentage ? a / 100 : a, 0, 1);
|
|
2995
2981
|
}
|
|
2996
|
-
function clamp(n, min, max) {
|
|
2982
|
+
function clamp$1(n, min, max) {
|
|
2997
2983
|
return Math.min(Math.max(min, n), max);
|
|
2998
2984
|
}
|
|
2999
2985
|
/**
|
|
@@ -3756,7 +3742,7 @@ const namedColors = {
|
|
|
3756
3742
|
50
|
|
3757
3743
|
]
|
|
3758
3744
|
};
|
|
3759
|
-
|
|
3745
|
+
|
|
3760
3746
|
//#region src/util/interpolate-primitives.ts
|
|
3761
3747
|
function interpolateNumber(from, to, t) {
|
|
3762
3748
|
return from + t * (to - from);
|
|
@@ -3766,7 +3752,7 @@ function interpolateArray(from, to, t) {
|
|
|
3766
3752
|
return interpolateNumber(d, to[i], t);
|
|
3767
3753
|
});
|
|
3768
3754
|
}
|
|
3769
|
-
|
|
3755
|
+
|
|
3770
3756
|
//#region src/expression/types/color.ts
|
|
3771
3757
|
/**
|
|
3772
3758
|
* Checks whether the specified color space is one of the supported interpolation color spaces.
|
|
@@ -3950,26 +3936,7 @@ var Color$1 = class Color {
|
|
|
3950
3936
|
}
|
|
3951
3937
|
}
|
|
3952
3938
|
};
|
|
3953
|
-
|
|
3954
|
-
//#region src/expression/types/collator.ts
|
|
3955
|
-
var Collator = class {
|
|
3956
|
-
constructor(caseSensitive, diacriticSensitive, locale) {
|
|
3957
|
-
if (caseSensitive) this.sensitivity = diacriticSensitive ? "variant" : "case";
|
|
3958
|
-
else this.sensitivity = diacriticSensitive ? "accent" : "base";
|
|
3959
|
-
this.locale = locale;
|
|
3960
|
-
this.collator = new Intl.Collator(this.locale ? this.locale : [], {
|
|
3961
|
-
sensitivity: this.sensitivity,
|
|
3962
|
-
usage: "search"
|
|
3963
|
-
});
|
|
3964
|
-
}
|
|
3965
|
-
compare(lhs, rhs) {
|
|
3966
|
-
return this.collator.compare(lhs, rhs);
|
|
3967
|
-
}
|
|
3968
|
-
resolvedLocale() {
|
|
3969
|
-
return new Intl.Collator(this.locale ? this.locale : []).resolvedOptions().locale;
|
|
3970
|
-
}
|
|
3971
|
-
};
|
|
3972
|
-
//#endregion
|
|
3939
|
+
|
|
3973
3940
|
//#region src/expression/types/formatted.ts
|
|
3974
3941
|
const VERTICAL_ALIGN_OPTIONS = [
|
|
3975
3942
|
"bottom",
|
|
@@ -4006,7 +3973,7 @@ var Formatted = class Formatted {
|
|
|
4006
3973
|
return this.sections.map((section) => section.text).join("");
|
|
4007
3974
|
}
|
|
4008
3975
|
};
|
|
4009
|
-
|
|
3976
|
+
|
|
4010
3977
|
//#region src/expression/types/padding.ts
|
|
4011
3978
|
/**
|
|
4012
3979
|
* A set of four numbers representing padding around a box. Create instances from
|
|
@@ -4050,14 +4017,12 @@ var Padding = class Padding {
|
|
|
4050
4017
|
input[1]
|
|
4051
4018
|
];
|
|
4052
4019
|
break;
|
|
4053
|
-
case 3:
|
|
4054
|
-
input
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
];
|
|
4060
|
-
break;
|
|
4020
|
+
case 3: input = [
|
|
4021
|
+
input[0],
|
|
4022
|
+
input[1],
|
|
4023
|
+
input[2],
|
|
4024
|
+
input[1]
|
|
4025
|
+
];
|
|
4061
4026
|
}
|
|
4062
4027
|
return new Padding(input);
|
|
4063
4028
|
}
|
|
@@ -4068,7 +4033,7 @@ var Padding = class Padding {
|
|
|
4068
4033
|
return new Padding(interpolateArray(from.values, to.values, t));
|
|
4069
4034
|
}
|
|
4070
4035
|
};
|
|
4071
|
-
|
|
4036
|
+
|
|
4072
4037
|
//#region src/expression/types/number_array.ts
|
|
4073
4038
|
/**
|
|
4074
4039
|
* An array of numbers. Create instances from
|
|
@@ -4098,7 +4063,7 @@ var NumberArray = class NumberArray {
|
|
|
4098
4063
|
return new NumberArray(interpolateArray(from.values, to.values, t));
|
|
4099
4064
|
}
|
|
4100
4065
|
};
|
|
4101
|
-
|
|
4066
|
+
|
|
4102
4067
|
//#region src/expression/types/color_array.ts
|
|
4103
4068
|
/**
|
|
4104
4069
|
* An array of colors. Create instances from
|
|
@@ -4141,7 +4106,7 @@ var ColorArray = class ColorArray {
|
|
|
4141
4106
|
return new ColorArray(colors);
|
|
4142
4107
|
}
|
|
4143
4108
|
};
|
|
4144
|
-
|
|
4109
|
+
|
|
4145
4110
|
//#region src/expression/runtime_error.ts
|
|
4146
4111
|
var RuntimeError = class extends Error {
|
|
4147
4112
|
constructor(message, path) {
|
|
@@ -4153,7 +4118,7 @@ var RuntimeError = class extends Error {
|
|
|
4153
4118
|
return this.message;
|
|
4154
4119
|
}
|
|
4155
4120
|
};
|
|
4156
|
-
|
|
4121
|
+
|
|
4157
4122
|
//#region src/expression/types/variable_anchor_offset_collection.ts
|
|
4158
4123
|
/** Set of valid anchor positions, as a set for validation */
|
|
4159
4124
|
const anchors = /* @__PURE__ */ new Set([
|
|
@@ -4205,7 +4170,7 @@ var VariableAnchorOffsetCollection = class VariableAnchorOffsetCollection {
|
|
|
4205
4170
|
return new VariableAnchorOffsetCollection(output);
|
|
4206
4171
|
}
|
|
4207
4172
|
};
|
|
4208
|
-
|
|
4173
|
+
|
|
4209
4174
|
//#region src/expression/types/resolved_image.ts
|
|
4210
4175
|
var ResolvedImage = class ResolvedImage {
|
|
4211
4176
|
constructor(options) {
|
|
@@ -4223,7 +4188,7 @@ var ResolvedImage = class ResolvedImage {
|
|
|
4223
4188
|
});
|
|
4224
4189
|
}
|
|
4225
4190
|
};
|
|
4226
|
-
|
|
4191
|
+
|
|
4227
4192
|
//#region src/expression/types/projection_definition.ts
|
|
4228
4193
|
var ProjectionDefinition = class ProjectionDefinition {
|
|
4229
4194
|
constructor(from, to, transition) {
|
|
@@ -4249,7 +4214,26 @@ var ProjectionDefinition = class ProjectionDefinition {
|
|
|
4249
4214
|
if (typeof input === "string") return new ProjectionDefinition(input, input, 1);
|
|
4250
4215
|
}
|
|
4251
4216
|
};
|
|
4252
|
-
|
|
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
|
+
|
|
4253
4237
|
//#region src/expression/values.ts
|
|
4254
4238
|
function validateRGBA(r, g, b, a) {
|
|
4255
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" ? [
|
|
@@ -4316,7 +4300,7 @@ function valueToString(value) {
|
|
|
4316
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();
|
|
4317
4301
|
else return JSON.stringify(value);
|
|
4318
4302
|
}
|
|
4319
|
-
|
|
4303
|
+
|
|
4320
4304
|
//#region src/expression/definitions/literal.ts
|
|
4321
4305
|
var Literal = class Literal {
|
|
4322
4306
|
constructor(type, value) {
|
|
@@ -4325,7 +4309,7 @@ var Literal = class Literal {
|
|
|
4325
4309
|
}
|
|
4326
4310
|
static parse(args, context) {
|
|
4327
4311
|
if (args.length !== 2) return context.error(`'literal' expression requires exactly one argument, but found ${args.length - 1} instead.`);
|
|
4328
|
-
if (!isValue(args[1])) return context.error(
|
|
4312
|
+
if (!isValue(args[1])) return context.error(`invalid value of type "${typeof args[1]}"`);
|
|
4329
4313
|
const value = args[1];
|
|
4330
4314
|
let type = typeOf(value);
|
|
4331
4315
|
const expected = context.expectedType;
|
|
@@ -4340,86 +4324,525 @@ var Literal = class Literal {
|
|
|
4340
4324
|
return true;
|
|
4341
4325
|
}
|
|
4342
4326
|
};
|
|
4343
|
-
|
|
4344
|
-
//#region src/expression/
|
|
4345
|
-
const
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
var
|
|
4352
|
-
constructor(
|
|
4353
|
-
this.
|
|
4354
|
-
this.
|
|
4355
|
-
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;
|
|
4356
4344
|
}
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
let i = 1;
|
|
4360
|
-
let type;
|
|
4361
|
-
const name = args[0];
|
|
4362
|
-
if (name === "array") {
|
|
4363
|
-
let itemType;
|
|
4364
|
-
if (args.length > 2) {
|
|
4365
|
-
const type = args[1];
|
|
4366
|
-
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);
|
|
4367
|
-
itemType = types$1[type];
|
|
4368
|
-
i++;
|
|
4369
|
-
} else itemType = ValueType;
|
|
4370
|
-
let N;
|
|
4371
|
-
if (args.length > 3) {
|
|
4372
|
-
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);
|
|
4373
|
-
N = args[2];
|
|
4374
|
-
i++;
|
|
4375
|
-
}
|
|
4376
|
-
type = array(itemType, N);
|
|
4377
|
-
} else {
|
|
4378
|
-
if (!types$1[name]) throw new Error(`Types doesn't contain name = ${name}`);
|
|
4379
|
-
type = types$1[name];
|
|
4380
|
-
}
|
|
4381
|
-
const parsed = [];
|
|
4382
|
-
for (; i < args.length; i++) {
|
|
4383
|
-
const input = context.parse(args[i], i, ValueType);
|
|
4384
|
-
if (!input) return null;
|
|
4385
|
-
parsed.push(input);
|
|
4386
|
-
}
|
|
4387
|
-
return new Assertion(type, parsed, context.key);
|
|
4345
|
+
id() {
|
|
4346
|
+
return this.feature && "id" in this.feature ? this.feature.id : null;
|
|
4388
4347
|
}
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
const value = this.args[i].evaluate(ctx);
|
|
4392
|
-
if (!checkSubtype(this.type, typeOf(value))) return value;
|
|
4393
|
-
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);
|
|
4394
|
-
}
|
|
4395
|
-
throw new Error();
|
|
4348
|
+
geometryType() {
|
|
4349
|
+
return this.feature ? typeof this.feature.type === "number" ? geometryTypes[this.feature.type] : this.feature.type : null;
|
|
4396
4350
|
}
|
|
4397
|
-
|
|
4398
|
-
this.
|
|
4351
|
+
geometry() {
|
|
4352
|
+
return this.feature && "geometry" in this.feature ? this.feature.geometry : null;
|
|
4399
4353
|
}
|
|
4400
|
-
|
|
4401
|
-
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;
|
|
4402
4367
|
}
|
|
4403
4368
|
};
|
|
4404
|
-
|
|
4405
|
-
//#region src/expression/
|
|
4406
|
-
const types = {
|
|
4407
|
-
"to-boolean": BooleanType,
|
|
4408
|
-
"to-color": ColorType,
|
|
4409
|
-
"to-number": NumberType,
|
|
4410
|
-
"to-string": StringType
|
|
4411
|
-
};
|
|
4369
|
+
|
|
4370
|
+
//#region src/expression/stops.ts
|
|
4412
4371
|
/**
|
|
4413
|
-
*
|
|
4414
|
-
* "to-color". Since these coercions can fail at runtime, they accept multiple
|
|
4415
|
-
* arguments, only evaluating one at a time until one succeeds.
|
|
4416
|
-
*
|
|
4372
|
+
* Returns the index of the last stop <= input, or 0 if it doesn't exist.
|
|
4417
4373
|
* @private
|
|
4418
4374
|
*/
|
|
4419
|
-
|
|
4420
|
-
|
|
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) {
|
|
4421
4397
|
this.type = type;
|
|
4422
|
-
this.
|
|
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;
|
|
4845
|
+
this.args = args;
|
|
4423
4846
|
this.key = key;
|
|
4424
4847
|
}
|
|
4425
4848
|
static parse(args, context) {
|
|
@@ -4515,177 +4938,13 @@ var Coercion = class Coercion {
|
|
|
4515
4938
|
}
|
|
4516
4939
|
}
|
|
4517
4940
|
eachChild(fn) {
|
|
4518
|
-
this.args.forEach(fn);
|
|
4519
|
-
}
|
|
4520
|
-
outputDefined() {
|
|
4521
|
-
return this.args.every((arg) => arg.outputDefined());
|
|
4522
|
-
}
|
|
4523
|
-
};
|
|
4524
|
-
//#endregion
|
|
4525
|
-
//#region src/expression/evaluation_context.ts
|
|
4526
|
-
const geometryTypes = [
|
|
4527
|
-
"Unknown",
|
|
4528
|
-
"Point",
|
|
4529
|
-
"LineString",
|
|
4530
|
-
"Polygon"
|
|
4531
|
-
];
|
|
4532
|
-
var EvaluationContext = class {
|
|
4533
|
-
constructor() {
|
|
4534
|
-
this.globals = null;
|
|
4535
|
-
this.feature = null;
|
|
4536
|
-
this.featureState = null;
|
|
4537
|
-
this.formattedSection = null;
|
|
4538
|
-
this._parseColorCache = /* @__PURE__ */ new Map();
|
|
4539
|
-
this.availableImages = null;
|
|
4540
|
-
this.canonical = null;
|
|
4541
|
-
}
|
|
4542
|
-
id() {
|
|
4543
|
-
return this.feature && "id" in this.feature ? this.feature.id : null;
|
|
4544
|
-
}
|
|
4545
|
-
geometryType() {
|
|
4546
|
-
return this.feature ? typeof this.feature.type === "number" ? geometryTypes[this.feature.type] : this.feature.type : null;
|
|
4547
|
-
}
|
|
4548
|
-
geometry() {
|
|
4549
|
-
return this.feature && "geometry" in this.feature ? this.feature.geometry : null;
|
|
4550
|
-
}
|
|
4551
|
-
canonicalID() {
|
|
4552
|
-
return this.canonical;
|
|
4553
|
-
}
|
|
4554
|
-
properties() {
|
|
4555
|
-
return this.feature && this.feature.properties || {};
|
|
4556
|
-
}
|
|
4557
|
-
parseColor(input) {
|
|
4558
|
-
let cached = this._parseColorCache.get(input);
|
|
4559
|
-
if (!cached) {
|
|
4560
|
-
cached = Color$1.parse(input);
|
|
4561
|
-
this._parseColorCache.set(input, cached);
|
|
4562
|
-
}
|
|
4563
|
-
return cached;
|
|
4564
|
-
}
|
|
4565
|
-
};
|
|
4566
|
-
//#endregion
|
|
4567
|
-
//#region src/expression/parsing_context.ts
|
|
4568
|
-
/**
|
|
4569
|
-
* State associated parsing at a given point in an expression tree.
|
|
4570
|
-
* @private
|
|
4571
|
-
*/
|
|
4572
|
-
var ParsingContext = class ParsingContext {
|
|
4573
|
-
constructor(registry, isConstantFunc, path = [], expectedType, scope = new Scope(), errors = []) {
|
|
4574
|
-
this.registry = registry;
|
|
4575
|
-
this.path = path;
|
|
4576
|
-
this.key = path.map((part) => `[${part}]`).join("");
|
|
4577
|
-
this.scope = scope;
|
|
4578
|
-
this.errors = errors;
|
|
4579
|
-
this.expectedType = expectedType;
|
|
4580
|
-
this._isConstant = isConstantFunc;
|
|
4581
|
-
}
|
|
4582
|
-
/**
|
|
4583
|
-
* @param expr the JSON expression to parse
|
|
4584
|
-
* @param index the optional argument index if this expression is an argument of a parent expression that's being parsed
|
|
4585
|
-
* @param options
|
|
4586
|
-
* @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.
|
|
4587
|
-
* @private
|
|
4588
|
-
*/
|
|
4589
|
-
parse(expr, index, expectedType, bindings, options = {}) {
|
|
4590
|
-
if (index) return this.concat(index, expectedType, bindings)._parse(expr, options);
|
|
4591
|
-
return this._parse(expr, options);
|
|
4592
|
-
}
|
|
4593
|
-
_parse(expr, options) {
|
|
4594
|
-
if (expr === null || typeof expr === "string" || typeof expr === "boolean" || typeof expr === "number") expr = ["literal", expr];
|
|
4595
|
-
const key = this.key;
|
|
4596
|
-
function annotate(parsed, type, typeAnnotation) {
|
|
4597
|
-
if (typeAnnotation === "assert") return new Assertion(type, [parsed], key);
|
|
4598
|
-
else if (typeAnnotation === "coerce") return new Coercion(type, [parsed], key);
|
|
4599
|
-
else return parsed;
|
|
4600
|
-
}
|
|
4601
|
-
if (Array.isArray(expr)) {
|
|
4602
|
-
if (expr.length === 0) return this.error("Expected an array with at least one element. If you wanted a literal array, use [\"literal\", []].");
|
|
4603
|
-
const op = expr[0];
|
|
4604
|
-
if (typeof op !== "string") {
|
|
4605
|
-
this.error(`Expression name must be a string, but found ${typeof op} instead. If you wanted a literal array, use ["literal", [...]].`, 0);
|
|
4606
|
-
return null;
|
|
4607
|
-
}
|
|
4608
|
-
const Expr = this.registry[op];
|
|
4609
|
-
if (Expr) {
|
|
4610
|
-
let parsed = Expr.parse(expr, this);
|
|
4611
|
-
if (!parsed) return null;
|
|
4612
|
-
if (this.expectedType) {
|
|
4613
|
-
const expected = this.expectedType;
|
|
4614
|
-
const actual = parsed.type;
|
|
4615
|
-
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");
|
|
4616
|
-
else if ("projectionDefinition" === expected.kind && [
|
|
4617
|
-
"string",
|
|
4618
|
-
"array",
|
|
4619
|
-
"value"
|
|
4620
|
-
].includes(actual.kind) || [
|
|
4621
|
-
"color",
|
|
4622
|
-
"formatted",
|
|
4623
|
-
"resolvedImage"
|
|
4624
|
-
].includes(expected.kind) && ["value", "string"].includes(actual.kind) || ["padding", "numberArray"].includes(expected.kind) && [
|
|
4625
|
-
"value",
|
|
4626
|
-
"number",
|
|
4627
|
-
"array"
|
|
4628
|
-
].includes(actual.kind) || "colorArray" === expected.kind && [
|
|
4629
|
-
"value",
|
|
4630
|
-
"string",
|
|
4631
|
-
"array"
|
|
4632
|
-
].includes(actual.kind) || "variableAnchorOffsetCollection" === expected.kind && ["value", "array"].includes(actual.kind)) parsed = annotate(parsed, expected, options.typeAnnotation || "coerce");
|
|
4633
|
-
else if (this.checkSubtype(expected, actual)) return null;
|
|
4634
|
-
}
|
|
4635
|
-
if (!(parsed instanceof Literal) && parsed.type.kind !== "resolvedImage" && this._isConstant(parsed)) {
|
|
4636
|
-
const ec = new EvaluationContext();
|
|
4637
|
-
try {
|
|
4638
|
-
parsed = new Literal(parsed.type, parsed.evaluate(ec));
|
|
4639
|
-
} catch (e) {
|
|
4640
|
-
this.error(e.message);
|
|
4641
|
-
return null;
|
|
4642
|
-
}
|
|
4643
|
-
}
|
|
4644
|
-
return parsed;
|
|
4645
|
-
}
|
|
4646
|
-
return this.error(`Unknown expression "${op}". If you wanted a literal array, use ["literal", [...]].`, 0);
|
|
4647
|
-
} else if (typeof expr === "undefined") return this.error("'undefined' value invalid. Use null instead.");
|
|
4648
|
-
else if (typeof expr === "object") return this.error("Bare objects invalid. Use [\"literal\", {...}] instead.");
|
|
4649
|
-
else return this.error(`Expected an array, but found ${typeof expr} instead.`);
|
|
4650
|
-
}
|
|
4651
|
-
/**
|
|
4652
|
-
* Returns a copy of this context suitable for parsing the subexpression at
|
|
4653
|
-
* index `index`, optionally appending to 'let' binding map.
|
|
4654
|
-
*
|
|
4655
|
-
* Note that `errors` property, intended for collecting errors while
|
|
4656
|
-
* parsing, is copied by reference rather than cloned.
|
|
4657
|
-
* @private
|
|
4658
|
-
*/
|
|
4659
|
-
concat(index, expectedType, bindings) {
|
|
4660
|
-
const path = typeof index === "number" ? this.path.concat(index) : this.path;
|
|
4661
|
-
const scope = bindings ? this.scope.concat(bindings) : this.scope;
|
|
4662
|
-
return new ParsingContext(this.registry, this._isConstant, path, expectedType || null, scope, this.errors);
|
|
4663
|
-
}
|
|
4664
|
-
/**
|
|
4665
|
-
* Push a parsing (or type checking) error into the `this.errors`
|
|
4666
|
-
* @param error The message
|
|
4667
|
-
* @param keys Optionally specify the source of the error at a child
|
|
4668
|
-
* of the current expression at `this.key`.
|
|
4669
|
-
* @private
|
|
4670
|
-
*/
|
|
4671
|
-
error(error, ...keys) {
|
|
4672
|
-
const key = `${this.key}${keys.map((k) => `[${k}]`).join("")}`;
|
|
4673
|
-
this.errors.push(new ExpressionParsingError(key, error));
|
|
4941
|
+
this.args.forEach(fn);
|
|
4674
4942
|
}
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
* error message and also pushes it to `this.errors`.
|
|
4678
|
-
* @param expected The expected type
|
|
4679
|
-
* @param t The actual type
|
|
4680
|
-
* @returns null if `t` is a subtype of `expected`; otherwise returns an error message
|
|
4681
|
-
*/
|
|
4682
|
-
checkSubtype(expected, t) {
|
|
4683
|
-
const error = checkSubtype(expected, t);
|
|
4684
|
-
if (error) this.error(error);
|
|
4685
|
-
return error;
|
|
4943
|
+
outputDefined() {
|
|
4944
|
+
return this.args.every((arg) => arg.outputDefined());
|
|
4686
4945
|
}
|
|
4687
4946
|
};
|
|
4688
|
-
|
|
4947
|
+
|
|
4689
4948
|
//#region src/expression/definitions/let.ts
|
|
4690
4949
|
var Let = class Let {
|
|
4691
4950
|
constructor(bindings, result) {
|
|
@@ -4719,7 +4978,7 @@ var Let = class Let {
|
|
|
4719
4978
|
return this.result.outputDefined();
|
|
4720
4979
|
}
|
|
4721
4980
|
};
|
|
4722
|
-
|
|
4981
|
+
|
|
4723
4982
|
//#region src/expression/definitions/var.ts
|
|
4724
4983
|
var Var = class Var {
|
|
4725
4984
|
constructor(name, boundExpression) {
|
|
@@ -4741,7 +5000,7 @@ var Var = class Var {
|
|
|
4741
5000
|
return false;
|
|
4742
5001
|
}
|
|
4743
5002
|
};
|
|
4744
|
-
|
|
5003
|
+
|
|
4745
5004
|
//#region src/expression/definitions/at.ts
|
|
4746
5005
|
var At = class At {
|
|
4747
5006
|
constructor(type, index, input, key) {
|
|
@@ -4774,7 +5033,7 @@ var At = class At {
|
|
|
4774
5033
|
return false;
|
|
4775
5034
|
}
|
|
4776
5035
|
};
|
|
4777
|
-
|
|
5036
|
+
|
|
4778
5037
|
//#region src/expression/definitions/in.ts
|
|
4779
5038
|
var In = class In {
|
|
4780
5039
|
constructor(needle, haystack, key) {
|
|
@@ -4818,7 +5077,7 @@ var In = class In {
|
|
|
4818
5077
|
return true;
|
|
4819
5078
|
}
|
|
4820
5079
|
};
|
|
4821
|
-
|
|
5080
|
+
|
|
4822
5081
|
//#region src/expression/definitions/index_of.ts
|
|
4823
5082
|
var IndexOf = class IndexOf {
|
|
4824
5083
|
constructor(needle, haystack, key, fromIndex) {
|
|
@@ -4873,7 +5132,7 @@ var IndexOf = class IndexOf {
|
|
|
4873
5132
|
return false;
|
|
4874
5133
|
}
|
|
4875
5134
|
};
|
|
4876
|
-
|
|
5135
|
+
|
|
4877
5136
|
//#region src/expression/definitions/match.ts
|
|
4878
5137
|
var Match = class Match {
|
|
4879
5138
|
constructor(inputType, outputType, input, cases, outputs, otherwise) {
|
|
@@ -4932,7 +5191,7 @@ var Match = class Match {
|
|
|
4932
5191
|
return this.outputs.every((out) => out.outputDefined()) && this.otherwise.outputDefined();
|
|
4933
5192
|
}
|
|
4934
5193
|
};
|
|
4935
|
-
|
|
5194
|
+
|
|
4936
5195
|
//#region src/expression/definitions/case.ts
|
|
4937
5196
|
var Case = class Case {
|
|
4938
5197
|
constructor(type, branches, otherwise) {
|
|
@@ -4954,344 +5213,71 @@ var Case = class Case {
|
|
|
4954
5213
|
branches.push([test, result]);
|
|
4955
5214
|
outputType = outputType || result.type;
|
|
4956
5215
|
}
|
|
4957
|
-
const otherwise = context.parse(args[args.length - 1], args.length - 1, outputType);
|
|
4958
|
-
if (!otherwise) return null;
|
|
4959
|
-
if (!outputType) throw new Error("Can't infer output type");
|
|
4960
|
-
return new Case(outputType, branches, otherwise);
|
|
4961
|
-
}
|
|
4962
|
-
evaluate(ctx) {
|
|
4963
|
-
for (const [test, expression] of this.branches) if (test.evaluate(ctx)) return expression.evaluate(ctx);
|
|
4964
|
-
return this.otherwise.evaluate(ctx);
|
|
4965
|
-
}
|
|
4966
|
-
eachChild(fn) {
|
|
4967
|
-
for (const [test, expression] of this.branches) {
|
|
4968
|
-
fn(test);
|
|
4969
|
-
fn(expression);
|
|
4970
|
-
}
|
|
4971
|
-
fn(this.otherwise);
|
|
4972
|
-
}
|
|
4973
|
-
outputDefined() {
|
|
4974
|
-
return this.branches.every(([_, out]) => out.outputDefined()) && this.otherwise.outputDefined();
|
|
4975
|
-
}
|
|
4976
|
-
};
|
|
4977
|
-
//#endregion
|
|
4978
|
-
//#region src/expression/definitions/slice.ts
|
|
4979
|
-
var Slice = class Slice {
|
|
4980
|
-
constructor(type, input, beginIndex, key, endIndex) {
|
|
4981
|
-
this.type = type;
|
|
4982
|
-
this.input = input;
|
|
4983
|
-
this.beginIndex = beginIndex;
|
|
4984
|
-
this.key = key;
|
|
4985
|
-
this.endIndex = endIndex;
|
|
4986
|
-
}
|
|
4987
|
-
static parse(args, context) {
|
|
4988
|
-
if (args.length <= 2 || args.length >= 5) return context.error(`Expected 2 or 3 arguments, but found ${args.length - 1} instead.`);
|
|
4989
|
-
const input = context.parse(args[1], 1, ValueType);
|
|
4990
|
-
const beginIndex = context.parse(args[2], 2, NumberType);
|
|
4991
|
-
if (!input || !beginIndex) return null;
|
|
4992
|
-
if (!isValidType(input.type, [
|
|
4993
|
-
array(ValueType),
|
|
4994
|
-
StringType,
|
|
4995
|
-
ValueType
|
|
4996
|
-
])) return context.error(`Expected first argument to be of type array or string, but found ${typeToString(input.type)} instead`);
|
|
4997
|
-
if (args.length === 4) {
|
|
4998
|
-
const endIndex = context.parse(args[3], 3, NumberType);
|
|
4999
|
-
if (!endIndex) return null;
|
|
5000
|
-
return new Slice(input.type, input, beginIndex, context.key, endIndex);
|
|
5001
|
-
} else return new Slice(input.type, input, beginIndex, context.key);
|
|
5002
|
-
}
|
|
5003
|
-
evaluate(ctx) {
|
|
5004
|
-
const input = this.input.evaluate(ctx);
|
|
5005
|
-
const beginIndex = this.beginIndex.evaluate(ctx);
|
|
5006
|
-
let endIndex;
|
|
5007
|
-
if (this.endIndex) endIndex = this.endIndex.evaluate(ctx);
|
|
5008
|
-
if (isValidNativeType(input, ["string"])) return [...input].slice(beginIndex, endIndex).join("");
|
|
5009
|
-
else if (isValidNativeType(input, ["array"])) return input.slice(beginIndex, endIndex);
|
|
5010
|
-
else throw new RuntimeError(`Expected first argument to be of type array or string, but found ${typeToString(typeOf(input))} instead.`, this.key);
|
|
5011
|
-
}
|
|
5012
|
-
eachChild(fn) {
|
|
5013
|
-
fn(this.input);
|
|
5014
|
-
fn(this.beginIndex);
|
|
5015
|
-
if (this.endIndex) fn(this.endIndex);
|
|
5016
|
-
}
|
|
5017
|
-
outputDefined() {
|
|
5018
|
-
return false;
|
|
5019
|
-
}
|
|
5020
|
-
};
|
|
5021
|
-
//#endregion
|
|
5022
|
-
//#region src/expression/stops.ts
|
|
5023
|
-
/**
|
|
5024
|
-
* Returns the index of the last stop <= input, or 0 if it doesn't exist.
|
|
5025
|
-
* @private
|
|
5026
|
-
*/
|
|
5027
|
-
function findStopLessThanOrEqualTo(stops, input, key) {
|
|
5028
|
-
const lastIndex = stops.length - 1;
|
|
5029
|
-
let lowerIndex = 0;
|
|
5030
|
-
let upperIndex = lastIndex;
|
|
5031
|
-
let currentIndex = 0;
|
|
5032
|
-
let currentValue, nextValue;
|
|
5033
|
-
while (lowerIndex <= upperIndex) {
|
|
5034
|
-
currentIndex = Math.floor((lowerIndex + upperIndex) / 2);
|
|
5035
|
-
currentValue = stops[currentIndex];
|
|
5036
|
-
nextValue = stops[currentIndex + 1];
|
|
5037
|
-
if (currentValue <= input) {
|
|
5038
|
-
if (currentIndex === lastIndex || input < nextValue) return currentIndex;
|
|
5039
|
-
lowerIndex = currentIndex + 1;
|
|
5040
|
-
} else if (currentValue > input) upperIndex = currentIndex - 1;
|
|
5041
|
-
else throw new RuntimeError("Input is not a number.", key);
|
|
5042
|
-
}
|
|
5043
|
-
return 0;
|
|
5044
|
-
}
|
|
5045
|
-
//#endregion
|
|
5046
|
-
//#region src/expression/definitions/step.ts
|
|
5047
|
-
var Step = class Step {
|
|
5048
|
-
constructor(type, input, stops, key) {
|
|
5049
|
-
this.type = type;
|
|
5050
|
-
this.input = input;
|
|
5051
|
-
this.key = key;
|
|
5052
|
-
this.labels = [];
|
|
5053
|
-
this.outputs = [];
|
|
5054
|
-
for (const [label, expression] of stops) {
|
|
5055
|
-
this.labels.push(label);
|
|
5056
|
-
this.outputs.push(expression);
|
|
5057
|
-
}
|
|
5058
|
-
}
|
|
5059
|
-
static parse(args, context) {
|
|
5060
|
-
if (args.length - 1 < 4) return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`);
|
|
5061
|
-
if ((args.length - 1) % 2 !== 0) return context.error("Expected an even number of arguments.");
|
|
5062
|
-
const input = context.parse(args[1], 1, NumberType);
|
|
5063
|
-
if (!input) return null;
|
|
5064
|
-
const stops = [];
|
|
5065
|
-
let outputType = null;
|
|
5066
|
-
if (context.expectedType && context.expectedType.kind !== "value") outputType = context.expectedType;
|
|
5067
|
-
for (let i = 1; i < args.length; i += 2) {
|
|
5068
|
-
const label = i === 1 ? -Infinity : args[i];
|
|
5069
|
-
const value = args[i + 1];
|
|
5070
|
-
const labelKey = i;
|
|
5071
|
-
const valueKey = i + 1;
|
|
5072
|
-
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);
|
|
5073
|
-
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);
|
|
5074
|
-
const parsed = context.parse(value, valueKey, outputType);
|
|
5075
|
-
if (!parsed) return null;
|
|
5076
|
-
outputType = outputType || parsed.type;
|
|
5077
|
-
stops.push([label, parsed]);
|
|
5078
|
-
}
|
|
5079
|
-
return new Step(outputType, input, stops, context.key);
|
|
5080
|
-
}
|
|
5081
|
-
evaluate(ctx) {
|
|
5082
|
-
const labels = this.labels;
|
|
5083
|
-
const outputs = this.outputs;
|
|
5084
|
-
if (labels.length === 1) return outputs[0].evaluate(ctx);
|
|
5085
|
-
const value = this.input.evaluate(ctx);
|
|
5086
|
-
if (value <= labels[0]) return outputs[0].evaluate(ctx);
|
|
5087
|
-
const stopCount = labels.length;
|
|
5088
|
-
if (value >= labels[stopCount - 1]) return outputs[stopCount - 1].evaluate(ctx);
|
|
5089
|
-
return outputs[findStopLessThanOrEqualTo(labels, value, this.key)].evaluate(ctx);
|
|
5090
|
-
}
|
|
5091
|
-
eachChild(fn) {
|
|
5092
|
-
fn(this.input);
|
|
5093
|
-
for (const expression of this.outputs) fn(expression);
|
|
5094
|
-
}
|
|
5095
|
-
outputDefined() {
|
|
5096
|
-
return this.outputs.every((out) => out.outputDefined());
|
|
5097
|
-
}
|
|
5098
|
-
};
|
|
5099
|
-
//#endregion
|
|
5100
|
-
//#region node_modules/@mapbox/unitbezier/index.js
|
|
5101
|
-
function unitBezier(p1x, p1y, p2x, p2y) {
|
|
5102
|
-
const cx = 3 * p1x;
|
|
5103
|
-
const bx = 3 * (p2x - p1x) - cx;
|
|
5104
|
-
const ax = 1 - cx - bx;
|
|
5105
|
-
const cy = 3 * p1y;
|
|
5106
|
-
const by = 3 * (p2y - p1y) - cy;
|
|
5107
|
-
const ay = 1 - cy - by;
|
|
5108
|
-
return function solve(x, epsilon = 1e-6) {
|
|
5109
|
-
if (x <= 0) return 0;
|
|
5110
|
-
if (x >= 1) return 1;
|
|
5111
|
-
let t = x;
|
|
5112
|
-
for (let i = 0; i < 8; i++) {
|
|
5113
|
-
const x2 = ((ax * t + bx) * t + cx) * t - x;
|
|
5114
|
-
if (Math.abs(x2) < epsilon) return ((ay * t + by) * t + cy) * t;
|
|
5115
|
-
const d2 = (3 * ax * t + 2 * bx) * t + cx;
|
|
5116
|
-
if (Math.abs(d2) < 1e-6) break;
|
|
5117
|
-
t -= x2 / d2;
|
|
5118
|
-
}
|
|
5119
|
-
let t0 = 0;
|
|
5120
|
-
let t1 = 1;
|
|
5121
|
-
t = x;
|
|
5122
|
-
for (let i = 0; i < 20; i++) {
|
|
5123
|
-
const x2 = ((ax * t + bx) * t + cx) * t;
|
|
5124
|
-
if (Math.abs(x2 - x) < epsilon) break;
|
|
5125
|
-
if (x > x2) t0 = t;
|
|
5126
|
-
else t1 = t;
|
|
5127
|
-
t = (t0 + t1) * .5;
|
|
5128
|
-
}
|
|
5129
|
-
return ((ay * t + by) * t + cy) * t;
|
|
5130
|
-
};
|
|
5131
|
-
}
|
|
5132
|
-
//#endregion
|
|
5133
|
-
//#region src/expression/definitions/interpolate.ts
|
|
5134
|
-
var Interpolate = class Interpolate {
|
|
5135
|
-
constructor(type, operator, interpolation, input, stops, key) {
|
|
5136
|
-
this.type = type;
|
|
5137
|
-
this.operator = operator;
|
|
5138
|
-
this.interpolation = interpolation;
|
|
5139
|
-
this.input = input;
|
|
5140
|
-
this.key = key;
|
|
5141
|
-
this.labels = [];
|
|
5142
|
-
this.outputs = [];
|
|
5143
|
-
for (const [label, expression] of stops) {
|
|
5144
|
-
this.labels.push(label);
|
|
5145
|
-
this.outputs.push(expression);
|
|
5146
|
-
}
|
|
5147
|
-
}
|
|
5148
|
-
static interpolationFactor(interpolation, input, lower, upper) {
|
|
5149
|
-
let t = 0;
|
|
5150
|
-
if (interpolation.name === "exponential") t = exponentialInterpolation(input, interpolation.base, lower, upper);
|
|
5151
|
-
else if (interpolation.name === "linear") t = exponentialInterpolation(input, 1, lower, upper);
|
|
5152
|
-
else if (interpolation.name === "cubic-bezier") {
|
|
5153
|
-
const c = interpolation.controlPoints;
|
|
5154
|
-
t = unitBezier(c[0], c[1], c[2], c[3])(exponentialInterpolation(input, 1, lower, upper));
|
|
5155
|
-
}
|
|
5156
|
-
return t;
|
|
5157
|
-
}
|
|
5158
|
-
static parse(args, context) {
|
|
5159
|
-
let [operator, interpolation, input, ...rest] = args;
|
|
5160
|
-
if (!Array.isArray(interpolation) || interpolation.length === 0) return context.error("Expected an interpolation type expression.", 1);
|
|
5161
|
-
if (interpolation[0] === "linear") interpolation = { name: "linear" };
|
|
5162
|
-
else if (interpolation[0] === "exponential") {
|
|
5163
|
-
const base = interpolation[1];
|
|
5164
|
-
if (typeof base !== "number") return context.error("Exponential interpolation requires a numeric base.", 1, 1);
|
|
5165
|
-
interpolation = {
|
|
5166
|
-
name: "exponential",
|
|
5167
|
-
base
|
|
5168
|
-
};
|
|
5169
|
-
} else if (interpolation[0] === "cubic-bezier") {
|
|
5170
|
-
const controlPoints = interpolation.slice(1);
|
|
5171
|
-
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);
|
|
5172
|
-
interpolation = {
|
|
5173
|
-
name: "cubic-bezier",
|
|
5174
|
-
controlPoints
|
|
5175
|
-
};
|
|
5176
|
-
} else return context.error(`Unknown interpolation type ${String(interpolation[0])}`, 1, 0);
|
|
5177
|
-
if (args.length - 1 < 4) return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`);
|
|
5178
|
-
if ((args.length - 1) % 2 !== 0) return context.error("Expected an even number of arguments.");
|
|
5179
|
-
input = context.parse(input, 2, NumberType);
|
|
5180
|
-
if (!input) return null;
|
|
5181
|
-
const stops = [];
|
|
5182
|
-
let outputType = null;
|
|
5183
|
-
if ((operator === "interpolate-hcl" || operator === "interpolate-lab") && context.expectedType != ColorArrayType) outputType = ColorType;
|
|
5184
|
-
else if (context.expectedType && context.expectedType.kind !== "value") outputType = context.expectedType;
|
|
5185
|
-
for (let i = 0; i < rest.length; i += 2) {
|
|
5186
|
-
const label = rest[i];
|
|
5187
|
-
const value = rest[i + 1];
|
|
5188
|
-
const labelKey = i + 3;
|
|
5189
|
-
const valueKey = i + 4;
|
|
5190
|
-
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);
|
|
5191
|
-
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);
|
|
5192
|
-
const parsed = context.parse(value, valueKey, outputType);
|
|
5193
|
-
if (!parsed) return null;
|
|
5194
|
-
outputType = outputType || parsed.type;
|
|
5195
|
-
stops.push([label, parsed]);
|
|
5196
|
-
}
|
|
5197
|
-
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.`);
|
|
5198
|
-
return new Interpolate(outputType, operator, interpolation, input, stops, context.key);
|
|
5216
|
+
const otherwise = context.parse(args[args.length - 1], args.length - 1, outputType);
|
|
5217
|
+
if (!otherwise) return null;
|
|
5218
|
+
if (!outputType) throw new Error("Can't infer output type");
|
|
5219
|
+
return new Case(outputType, branches, otherwise);
|
|
5199
5220
|
}
|
|
5200
5221
|
evaluate(ctx) {
|
|
5201
|
-
const
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
const index = findStopLessThanOrEqualTo(labels, value, this.key);
|
|
5209
|
-
const lower = labels[index];
|
|
5210
|
-
const upper = labels[index + 1];
|
|
5211
|
-
const t = Interpolate.interpolationFactor(this.interpolation, value, lower, upper);
|
|
5212
|
-
const outputLower = outputs[index].evaluate(ctx);
|
|
5213
|
-
const outputUpper = outputs[index + 1].evaluate(ctx);
|
|
5214
|
-
switch (this.operator) {
|
|
5215
|
-
case "interpolate": switch (this.type.kind) {
|
|
5216
|
-
case "number": return interpolateNumber(outputLower, outputUpper, t);
|
|
5217
|
-
case "color": return Color$1.interpolate(outputLower, outputUpper, t);
|
|
5218
|
-
case "padding": return Padding.interpolate(outputLower, outputUpper, t);
|
|
5219
|
-
case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t);
|
|
5220
|
-
case "numberArray": return NumberArray.interpolate(outputLower, outputUpper, t);
|
|
5221
|
-
case "variableAnchorOffsetCollection": return VariableAnchorOffsetCollection.interpolate(outputLower, outputUpper, t, this.key);
|
|
5222
|
-
case "array": return interpolateArray(outputLower, outputUpper, t);
|
|
5223
|
-
case "projectionDefinition": return ProjectionDefinition.interpolate(outputLower, outputUpper, t);
|
|
5224
|
-
}
|
|
5225
|
-
case "interpolate-hcl": switch (this.type.kind) {
|
|
5226
|
-
case "color": return Color$1.interpolate(outputLower, outputUpper, t, "hcl");
|
|
5227
|
-
case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t, "hcl");
|
|
5228
|
-
}
|
|
5229
|
-
case "interpolate-lab": switch (this.type.kind) {
|
|
5230
|
-
case "color": return Color$1.interpolate(outputLower, outputUpper, t, "lab");
|
|
5231
|
-
case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t, "lab");
|
|
5232
|
-
}
|
|
5222
|
+
for (const [test, expression] of this.branches) if (test.evaluate(ctx)) return expression.evaluate(ctx);
|
|
5223
|
+
return this.otherwise.evaluate(ctx);
|
|
5224
|
+
}
|
|
5225
|
+
eachChild(fn) {
|
|
5226
|
+
for (const [test, expression] of this.branches) {
|
|
5227
|
+
fn(test);
|
|
5228
|
+
fn(expression);
|
|
5233
5229
|
}
|
|
5230
|
+
fn(this.otherwise);
|
|
5231
|
+
}
|
|
5232
|
+
outputDefined() {
|
|
5233
|
+
return this.branches.every(([_, out]) => out.outputDefined()) && this.otherwise.outputDefined();
|
|
5234
|
+
}
|
|
5235
|
+
};
|
|
5236
|
+
|
|
5237
|
+
//#region src/expression/definitions/slice.ts
|
|
5238
|
+
var Slice = class Slice {
|
|
5239
|
+
constructor(type, input, beginIndex, key, endIndex) {
|
|
5240
|
+
this.type = type;
|
|
5241
|
+
this.input = input;
|
|
5242
|
+
this.beginIndex = beginIndex;
|
|
5243
|
+
this.key = key;
|
|
5244
|
+
this.endIndex = endIndex;
|
|
5245
|
+
}
|
|
5246
|
+
static parse(args, context) {
|
|
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);
|
|
5261
|
+
}
|
|
5262
|
+
evaluate(ctx) {
|
|
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);
|
|
5234
5270
|
}
|
|
5235
5271
|
eachChild(fn) {
|
|
5236
5272
|
fn(this.input);
|
|
5237
|
-
|
|
5273
|
+
fn(this.beginIndex);
|
|
5274
|
+
if (this.endIndex) fn(this.endIndex);
|
|
5238
5275
|
}
|
|
5239
5276
|
outputDefined() {
|
|
5240
|
-
return
|
|
5277
|
+
return false;
|
|
5241
5278
|
}
|
|
5242
5279
|
};
|
|
5243
|
-
|
|
5244
|
-
* Returns a ratio that can be used to interpolate between exponential function
|
|
5245
|
-
* stops.
|
|
5246
|
-
* 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,
|
|
5247
|
-
* and `a` and `b` are constants affording sufficient degrees of freedom to fit
|
|
5248
|
-
* the function to the given stops.
|
|
5249
|
-
*
|
|
5250
|
-
* Here's a bit of algebra that lets us compute `f(x)` directly from the stop
|
|
5251
|
-
* values without explicitly solving for `a` and `b`:
|
|
5252
|
-
*
|
|
5253
|
-
* First stop value: `f(x0) = y0 = a * base^x0 + b`
|
|
5254
|
-
* Second stop value: `f(x1) = y1 = a * base^x1 + b`
|
|
5255
|
-
* => `y1 - y0 = a(base^x1 - base^x0)`
|
|
5256
|
-
* => `a = (y1 - y0)/(base^x1 - base^x0)`
|
|
5257
|
-
*
|
|
5258
|
-
* Desired value: `f(x) = y = a * base^x + b`
|
|
5259
|
-
* => `f(x) = y0 + a * (base^x - base^x0)`
|
|
5260
|
-
*
|
|
5261
|
-
* From the above, we can replace the `a` in `a * (base^x - base^x0)` and do a
|
|
5262
|
-
* little algebra:
|
|
5263
|
-
* ```
|
|
5264
|
-
* a * (base^x - base^x0) = (y1 - y0)/(base^x1 - base^x0) * (base^x - base^x0)
|
|
5265
|
-
* = (y1 - y0) * (base^x - base^x0) / (base^x1 - base^x0)
|
|
5266
|
-
* ```
|
|
5267
|
-
*
|
|
5268
|
-
* If we let `(base^x - base^x0) / (base^x1 base^x0)`, then we have
|
|
5269
|
-
* `f(x) = y0 + (y1 - y0) * ratio`. In other words, `ratio` may be treated as
|
|
5270
|
-
* an interpolation factor between the two stops' output values.
|
|
5271
|
-
*
|
|
5272
|
-
* (Note: a slightly different form for `ratio`,
|
|
5273
|
-
* `(base^(x-x0) - 1) / (base^(x1-x0) - 1) `, is equivalent, but requires fewer
|
|
5274
|
-
* expensive `Math.pow()` operations.)
|
|
5275
|
-
*
|
|
5276
|
-
* @private
|
|
5277
|
-
*/
|
|
5278
|
-
function exponentialInterpolation(input, base, lowerValue, upperValue) {
|
|
5279
|
-
const difference = upperValue - lowerValue;
|
|
5280
|
-
const progress = input - lowerValue;
|
|
5281
|
-
if (difference === 0) return 0;
|
|
5282
|
-
else if (base === 1) return progress / difference;
|
|
5283
|
-
else return (Math.pow(base, progress) - 1) / (Math.pow(base, difference) - 1);
|
|
5284
|
-
}
|
|
5285
|
-
const interpolateFactory = {
|
|
5286
|
-
color: Color$1.interpolate,
|
|
5287
|
-
number: interpolateNumber,
|
|
5288
|
-
padding: Padding.interpolate,
|
|
5289
|
-
numberArray: NumberArray.interpolate,
|
|
5290
|
-
colorArray: ColorArray.interpolate,
|
|
5291
|
-
variableAnchorOffsetCollection: VariableAnchorOffsetCollection.interpolate,
|
|
5292
|
-
array: interpolateArray
|
|
5293
|
-
};
|
|
5294
|
-
//#endregion
|
|
5280
|
+
|
|
5295
5281
|
//#region src/expression/definitions/coalesce.ts
|
|
5296
5282
|
var Coalesce = class Coalesce {
|
|
5297
5283
|
constructor(type, args) {
|
|
@@ -5336,7 +5322,7 @@ var Coalesce = class Coalesce {
|
|
|
5336
5322
|
return this.args.every((arg) => arg.outputDefined());
|
|
5337
5323
|
}
|
|
5338
5324
|
};
|
|
5339
|
-
|
|
5325
|
+
|
|
5340
5326
|
//#region src/expression/definitions/comparison.ts
|
|
5341
5327
|
function isComparableType(op, type) {
|
|
5342
5328
|
if (op === "==" || op === "!=") return type.kind === "boolean" || type.kind === "string" || type.kind === "number" || type.kind === "null" || type.kind === "value";
|
|
@@ -5459,7 +5445,7 @@ const LessThan = makeComparison("<", lt, ltCollate);
|
|
|
5459
5445
|
const GreaterThan = makeComparison(">", gt, gtCollate);
|
|
5460
5446
|
const LessThanOrEqual = makeComparison("<=", lteq, lteqCollate);
|
|
5461
5447
|
const GreaterThanOrEqual = makeComparison(">=", gteq, gteqCollate);
|
|
5462
|
-
|
|
5448
|
+
|
|
5463
5449
|
//#region src/expression/definitions/collator.ts
|
|
5464
5450
|
var CollatorExpression = class CollatorExpression {
|
|
5465
5451
|
constructor(caseSensitive, diacriticSensitive, locale) {
|
|
@@ -5495,7 +5481,7 @@ var CollatorExpression = class CollatorExpression {
|
|
|
5495
5481
|
return false;
|
|
5496
5482
|
}
|
|
5497
5483
|
};
|
|
5498
|
-
|
|
5484
|
+
|
|
5499
5485
|
//#region src/expression/definitions/number_format.ts
|
|
5500
5486
|
var NumberFormat = class NumberFormat {
|
|
5501
5487
|
constructor(number, locale, currency, unit, minFractionDigits, maxFractionDigits) {
|
|
@@ -5507,143 +5493,62 @@ var NumberFormat = class NumberFormat {
|
|
|
5507
5493
|
this.minFractionDigits = minFractionDigits;
|
|
5508
5494
|
this.maxFractionDigits = maxFractionDigits;
|
|
5509
5495
|
}
|
|
5510
|
-
static parse(args, context) {
|
|
5511
|
-
if (args.length !== 3) return context.error("Expected two arguments.");
|
|
5512
|
-
const number = context.parse(args[1], 1, NumberType);
|
|
5513
|
-
if (!number) return null;
|
|
5514
|
-
const options = args[2];
|
|
5515
|
-
if (typeof options !== "object" || Array.isArray(options)) return context.error("NumberFormat options argument must be an object.");
|
|
5516
|
-
let locale = null;
|
|
5517
|
-
if (options["locale"]) {
|
|
5518
|
-
locale = context.parse(options["locale"], 1, StringType);
|
|
5519
|
-
if (!locale) return null;
|
|
5520
|
-
}
|
|
5521
|
-
let currency = null;
|
|
5522
|
-
if (options["currency"]) {
|
|
5523
|
-
currency = context.parse(options["currency"], 1, StringType);
|
|
5524
|
-
if (!currency) return null;
|
|
5525
|
-
}
|
|
5526
|
-
let unit = null;
|
|
5527
|
-
if (options["unit"]) {
|
|
5528
|
-
unit = context.parse(options["unit"], 1, StringType);
|
|
5529
|
-
if (!unit) return null;
|
|
5530
|
-
}
|
|
5531
|
-
if (currency && unit) return context.error("NumberFormat options `currency` and `unit` are mutually exclusive");
|
|
5532
|
-
let minFractionDigits = null;
|
|
5533
|
-
if (options["min-fraction-digits"]) {
|
|
5534
|
-
minFractionDigits = context.parse(options["min-fraction-digits"], 1, NumberType);
|
|
5535
|
-
if (!minFractionDigits) return null;
|
|
5536
|
-
}
|
|
5537
|
-
let maxFractionDigits = null;
|
|
5538
|
-
if (options["max-fraction-digits"]) {
|
|
5539
|
-
maxFractionDigits = context.parse(options["max-fraction-digits"], 1, NumberType);
|
|
5540
|
-
if (!maxFractionDigits) return null;
|
|
5541
|
-
}
|
|
5542
|
-
return new NumberFormat(number, locale, currency, unit, minFractionDigits, maxFractionDigits);
|
|
5543
|
-
}
|
|
5544
|
-
evaluate(ctx) {
|
|
5545
|
-
return new Intl.NumberFormat(this.locale ? this.locale.evaluate(ctx) : [], {
|
|
5546
|
-
style: this.currency ? "currency" : this.unit ? "unit" : "decimal",
|
|
5547
|
-
currency: this.currency ? this.currency.evaluate(ctx) : void 0,
|
|
5548
|
-
unit: this.unit ? this.unit.evaluate(ctx) : void 0,
|
|
5549
|
-
minimumFractionDigits: this.minFractionDigits ? this.minFractionDigits.evaluate(ctx) : void 0,
|
|
5550
|
-
maximumFractionDigits: this.maxFractionDigits ? this.maxFractionDigits.evaluate(ctx) : void 0
|
|
5551
|
-
}).format(this.number.evaluate(ctx));
|
|
5552
|
-
}
|
|
5553
|
-
eachChild(fn) {
|
|
5554
|
-
fn(this.number);
|
|
5555
|
-
if (this.locale) fn(this.locale);
|
|
5556
|
-
if (this.currency) fn(this.currency);
|
|
5557
|
-
if (this.unit) fn(this.unit);
|
|
5558
|
-
if (this.minFractionDigits) fn(this.minFractionDigits);
|
|
5559
|
-
if (this.maxFractionDigits) fn(this.maxFractionDigits);
|
|
5560
|
-
}
|
|
5561
|
-
outputDefined() {
|
|
5562
|
-
return false;
|
|
5563
|
-
}
|
|
5564
|
-
};
|
|
5565
|
-
//#endregion
|
|
5566
|
-
//#region src/expression/definitions/format.ts
|
|
5567
|
-
var FormatExpression = class FormatExpression {
|
|
5568
|
-
constructor(sections) {
|
|
5569
|
-
this.type = FormattedType;
|
|
5570
|
-
this.sections = sections;
|
|
5571
|
-
}
|
|
5572
|
-
static parse(args, context) {
|
|
5573
|
-
if (args.length < 2) return context.error("Expected at least one argument.");
|
|
5574
|
-
const firstArg = args[1];
|
|
5575
|
-
if (!Array.isArray(firstArg) && typeof firstArg === "object") return context.error("First argument must be an image or text section.");
|
|
5576
|
-
const sections = [];
|
|
5577
|
-
let nextTokenMayBeObject = false;
|
|
5578
|
-
for (let i = 1; i <= args.length - 1; ++i) {
|
|
5579
|
-
const arg = args[i];
|
|
5580
|
-
if (nextTokenMayBeObject && typeof arg === "object" && !Array.isArray(arg)) {
|
|
5581
|
-
nextTokenMayBeObject = false;
|
|
5582
|
-
let scale = null;
|
|
5583
|
-
if (arg["font-scale"]) {
|
|
5584
|
-
scale = context.parse(arg["font-scale"], 1, NumberType);
|
|
5585
|
-
if (!scale) return null;
|
|
5586
|
-
}
|
|
5587
|
-
let font = null;
|
|
5588
|
-
if (arg["text-font"]) {
|
|
5589
|
-
font = context.parse(arg["text-font"], 1, array(StringType));
|
|
5590
|
-
if (!font) return null;
|
|
5591
|
-
}
|
|
5592
|
-
let textColor = null;
|
|
5593
|
-
if (arg["text-color"]) {
|
|
5594
|
-
textColor = context.parse(arg["text-color"], 1, ColorType);
|
|
5595
|
-
if (!textColor) return null;
|
|
5596
|
-
}
|
|
5597
|
-
let verticalAlign = null;
|
|
5598
|
-
if (arg["vertical-align"]) {
|
|
5599
|
-
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.`);
|
|
5600
|
-
verticalAlign = context.parse(arg["vertical-align"], 1, StringType);
|
|
5601
|
-
if (!verticalAlign) return null;
|
|
5602
|
-
}
|
|
5603
|
-
const lastExpression = sections[sections.length - 1];
|
|
5604
|
-
lastExpression.scale = scale;
|
|
5605
|
-
lastExpression.font = font;
|
|
5606
|
-
lastExpression.textColor = textColor;
|
|
5607
|
-
lastExpression.verticalAlign = verticalAlign;
|
|
5608
|
-
} else {
|
|
5609
|
-
const content = context.parse(args[i], 1, ValueType);
|
|
5610
|
-
if (!content) return null;
|
|
5611
|
-
const kind = content.type.kind;
|
|
5612
|
-
if (kind !== "string" && kind !== "value" && kind !== "null" && kind !== "resolvedImage") return context.error("Formatted text type must be 'string', 'value', 'image' or 'null'.");
|
|
5613
|
-
nextTokenMayBeObject = true;
|
|
5614
|
-
sections.push({
|
|
5615
|
-
content,
|
|
5616
|
-
scale: null,
|
|
5617
|
-
font: null,
|
|
5618
|
-
textColor: null,
|
|
5619
|
-
verticalAlign: null
|
|
5620
|
-
});
|
|
5621
|
-
}
|
|
5496
|
+
static parse(args, context) {
|
|
5497
|
+
if (args.length !== 3) return context.error("Expected two arguments.");
|
|
5498
|
+
const number = context.parse(args[1], 1, NumberType);
|
|
5499
|
+
if (!number) return null;
|
|
5500
|
+
const options = args[2];
|
|
5501
|
+
if (typeof options !== "object" || Array.isArray(options)) return context.error("NumberFormat options argument must be an object.");
|
|
5502
|
+
let locale = null;
|
|
5503
|
+
if (options["locale"]) {
|
|
5504
|
+
locale = context.parse(options["locale"], 1, StringType);
|
|
5505
|
+
if (!locale) return null;
|
|
5622
5506
|
}
|
|
5623
|
-
|
|
5507
|
+
let currency = null;
|
|
5508
|
+
if (options["currency"]) {
|
|
5509
|
+
currency = context.parse(options["currency"], 1, StringType);
|
|
5510
|
+
if (!currency) return null;
|
|
5511
|
+
}
|
|
5512
|
+
let unit = null;
|
|
5513
|
+
if (options["unit"]) {
|
|
5514
|
+
unit = context.parse(options["unit"], 1, StringType);
|
|
5515
|
+
if (!unit) return null;
|
|
5516
|
+
}
|
|
5517
|
+
if (currency && unit) return context.error("NumberFormat options `currency` and `unit` are mutually exclusive");
|
|
5518
|
+
let minFractionDigits = null;
|
|
5519
|
+
if (options["min-fraction-digits"]) {
|
|
5520
|
+
minFractionDigits = context.parse(options["min-fraction-digits"], 1, NumberType);
|
|
5521
|
+
if (!minFractionDigits) return null;
|
|
5522
|
+
}
|
|
5523
|
+
let maxFractionDigits = null;
|
|
5524
|
+
if (options["max-fraction-digits"]) {
|
|
5525
|
+
maxFractionDigits = context.parse(options["max-fraction-digits"], 1, NumberType);
|
|
5526
|
+
if (!maxFractionDigits) return null;
|
|
5527
|
+
}
|
|
5528
|
+
return new NumberFormat(number, locale, currency, unit, minFractionDigits, maxFractionDigits);
|
|
5624
5529
|
}
|
|
5625
5530
|
evaluate(ctx) {
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5531
|
+
return new Intl.NumberFormat(this.locale ? this.locale.evaluate(ctx) : [], {
|
|
5532
|
+
style: this.currency ? "currency" : this.unit ? "unit" : "decimal",
|
|
5533
|
+
currency: this.currency ? this.currency.evaluate(ctx) : void 0,
|
|
5534
|
+
unit: this.unit ? this.unit.evaluate(ctx) : void 0,
|
|
5535
|
+
minimumFractionDigits: this.minFractionDigits ? this.minFractionDigits.evaluate(ctx) : void 0,
|
|
5536
|
+
maximumFractionDigits: this.maxFractionDigits ? this.maxFractionDigits.evaluate(ctx) : void 0
|
|
5537
|
+
}).format(this.number.evaluate(ctx));
|
|
5632
5538
|
}
|
|
5633
5539
|
eachChild(fn) {
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
}
|
|
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);
|
|
5641
5546
|
}
|
|
5642
5547
|
outputDefined() {
|
|
5643
5548
|
return false;
|
|
5644
5549
|
}
|
|
5645
5550
|
};
|
|
5646
|
-
|
|
5551
|
+
|
|
5647
5552
|
//#region src/expression/definitions/image.ts
|
|
5648
5553
|
var ImageExpression = class ImageExpression {
|
|
5649
5554
|
constructor(input) {
|
|
@@ -5669,7 +5574,7 @@ var ImageExpression = class ImageExpression {
|
|
|
5669
5574
|
return false;
|
|
5670
5575
|
}
|
|
5671
5576
|
};
|
|
5672
|
-
|
|
5577
|
+
|
|
5673
5578
|
//#region src/expression/definitions/length.ts
|
|
5674
5579
|
var Length = class Length {
|
|
5675
5580
|
constructor(input, key) {
|
|
@@ -5697,7 +5602,7 @@ var Length = class Length {
|
|
|
5697
5602
|
return false;
|
|
5698
5603
|
}
|
|
5699
5604
|
};
|
|
5700
|
-
|
|
5605
|
+
|
|
5701
5606
|
//#region src/util/geometry_util.ts
|
|
5702
5607
|
const EXTENT = 8192;
|
|
5703
5608
|
function getTileCoordinates(p, canonical) {
|
|
@@ -5793,7 +5698,7 @@ function twoSided(p1, p2, q1, q2) {
|
|
|
5793
5698
|
if (det1 > 0 && det2 < 0 || det1 < 0 && det2 > 0) return true;
|
|
5794
5699
|
return false;
|
|
5795
5700
|
}
|
|
5796
|
-
|
|
5701
|
+
|
|
5797
5702
|
//#region src/expression/definitions/within.ts
|
|
5798
5703
|
function getTilePolygon(coordinates, bbox, canonical) {
|
|
5799
5704
|
const polygon = [];
|
|
@@ -5955,7 +5860,7 @@ var Within = class Within {
|
|
|
5955
5860
|
return true;
|
|
5956
5861
|
}
|
|
5957
5862
|
};
|
|
5958
|
-
|
|
5863
|
+
|
|
5959
5864
|
//#region node_modules/tinyqueue/index.js
|
|
5960
5865
|
var TinyQueue = class {
|
|
5961
5866
|
constructor(data = [], compare = (a, b) => a < b ? -1 : a > b ? 1 : 0) {
|
|
@@ -6008,54 +5913,10 @@ var TinyQueue = class {
|
|
|
6008
5913
|
data[pos] = item;
|
|
6009
5914
|
}
|
|
6010
5915
|
};
|
|
6011
|
-
|
|
6012
|
-
//#region src/util/classify_rings.ts
|
|
6013
|
-
/**
|
|
6014
|
-
* Classifies an array of rings into polygons with outer rings and holes
|
|
6015
|
-
* @param rings - the rings to classify
|
|
6016
|
-
* @param maxRings - the maximum number of rings to include in a polygon, use 0 to include all rings
|
|
6017
|
-
* @returns an array of polygons with internal rings as holes
|
|
6018
|
-
*/
|
|
6019
|
-
function classifyRings$1(rings, maxRings) {
|
|
6020
|
-
if (rings.length <= 1) return [rings];
|
|
6021
|
-
const polygons = [];
|
|
6022
|
-
let polygon;
|
|
6023
|
-
let ccw;
|
|
6024
|
-
for (const ring of rings) {
|
|
6025
|
-
const area = calculateSignedArea(ring);
|
|
6026
|
-
if (area === 0) continue;
|
|
6027
|
-
ring.area = Math.abs(area);
|
|
6028
|
-
if (ccw === void 0) ccw = area < 0;
|
|
6029
|
-
if (ccw === area < 0) {
|
|
6030
|
-
if (polygon) polygons.push(polygon);
|
|
6031
|
-
polygon = [ring];
|
|
6032
|
-
} else polygon.push(ring);
|
|
6033
|
-
}
|
|
6034
|
-
if (polygon) polygons.push(polygon);
|
|
6035
|
-
return polygons;
|
|
6036
|
-
}
|
|
6037
|
-
/**
|
|
6038
|
-
* Returns the signed area for the polygon ring. Positive areas are exterior rings and
|
|
6039
|
-
* have a clockwise winding. Negative areas are interior rings and have a counter clockwise
|
|
6040
|
-
* ordering.
|
|
6041
|
-
*
|
|
6042
|
-
* @param ring - Exterior or interior ring
|
|
6043
|
-
* @returns Signed area
|
|
6044
|
-
*/
|
|
6045
|
-
function calculateSignedArea(ring) {
|
|
6046
|
-
let sum = 0;
|
|
6047
|
-
for (let i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) {
|
|
6048
|
-
p1 = ring[i];
|
|
6049
|
-
p2 = ring[j];
|
|
6050
|
-
sum += (p2.x - p1.x) * (p1.y + p2.y);
|
|
6051
|
-
}
|
|
6052
|
-
return sum;
|
|
6053
|
-
}
|
|
6054
|
-
//#endregion
|
|
5916
|
+
|
|
6055
5917
|
//#region src/util/cheap_ruler.ts
|
|
6056
5918
|
const RE = 6378.137;
|
|
6057
|
-
const
|
|
6058
|
-
const E2 = FE * (2 - FE);
|
|
5919
|
+
const E2 = .0066943799901413165;
|
|
6059
5920
|
const RAD = Math.PI / 180;
|
|
6060
5921
|
var CheapRuler = class {
|
|
6061
5922
|
constructor(lat) {
|
|
@@ -6064,7 +5925,7 @@ var CheapRuler = class {
|
|
|
6064
5925
|
const w2 = 1 / (1 - E2 * (1 - coslat * coslat));
|
|
6065
5926
|
const w = Math.sqrt(w2);
|
|
6066
5927
|
this.kx = m * w * coslat;
|
|
6067
|
-
this.ky = m * w * w2 *
|
|
5928
|
+
this.ky = m * w * w2 * .9933056200098587;
|
|
6068
5929
|
}
|
|
6069
5930
|
/**
|
|
6070
5931
|
* Given two points of the form [longitude, latitude], returns the distance.
|
|
@@ -6135,7 +5996,7 @@ var CheapRuler = class {
|
|
|
6135
5996
|
return deg;
|
|
6136
5997
|
}
|
|
6137
5998
|
};
|
|
6138
|
-
|
|
5999
|
+
|
|
6139
6000
|
//#region src/expression/definitions/distance.ts
|
|
6140
6001
|
const MinPointsSize = 100;
|
|
6141
6002
|
const MinLinePointsSize = 50;
|
|
@@ -6403,9 +6264,7 @@ function pointToGeometryDistance(ctx, geometries) {
|
|
|
6403
6264
|
case "LineString":
|
|
6404
6265
|
dist = Math.min(dist, pointSetToPointSetDistance(pointPosition, false, geometry.coordinates, true, ruler, dist));
|
|
6405
6266
|
break;
|
|
6406
|
-
case "Polygon":
|
|
6407
|
-
dist = Math.min(dist, pointsToPolygonDistance(pointPosition, false, geometry.coordinates, ruler, dist));
|
|
6408
|
-
break;
|
|
6267
|
+
case "Polygon": dist = Math.min(dist, pointsToPolygonDistance(pointPosition, false, geometry.coordinates, ruler, dist));
|
|
6409
6268
|
}
|
|
6410
6269
|
if (dist === 0) return dist;
|
|
6411
6270
|
}
|
|
@@ -6425,9 +6284,7 @@ function lineStringToGeometryDistance(ctx, geometries) {
|
|
|
6425
6284
|
case "LineString":
|
|
6426
6285
|
dist = Math.min(dist, pointSetToPointSetDistance(linePositions, true, geometry.coordinates, true, ruler, dist));
|
|
6427
6286
|
break;
|
|
6428
|
-
case "Polygon":
|
|
6429
|
-
dist = Math.min(dist, pointsToPolygonDistance(linePositions, true, geometry.coordinates, ruler, dist));
|
|
6430
|
-
break;
|
|
6287
|
+
case "Polygon": dist = Math.min(dist, pointsToPolygonDistance(linePositions, true, geometry.coordinates, ruler, dist));
|
|
6431
6288
|
}
|
|
6432
6289
|
if (dist === 0) return dist;
|
|
6433
6290
|
}
|
|
@@ -6451,9 +6308,7 @@ function polygonToGeometryDistance(ctx, geometries) {
|
|
|
6451
6308
|
case "LineString":
|
|
6452
6309
|
dist = Math.min(dist, pointsToPolygonDistance(geometry.coordinates, true, polygon, ruler, dist));
|
|
6453
6310
|
break;
|
|
6454
|
-
case "Polygon":
|
|
6455
|
-
dist = Math.min(dist, polygonToPolygonDistance(polygon, geometry.coordinates, ruler, dist));
|
|
6456
|
-
break;
|
|
6311
|
+
case "Polygon": dist = Math.min(dist, polygonToPolygonDistance(polygon, geometry.coordinates, ruler, dist));
|
|
6457
6312
|
}
|
|
6458
6313
|
if (dist === 0) return dist;
|
|
6459
6314
|
}
|
|
@@ -6509,72 +6364,270 @@ var Distance = class Distance {
|
|
|
6509
6364
|
return true;
|
|
6510
6365
|
}
|
|
6511
6366
|
};
|
|
6512
|
-
|
|
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
|
+
|
|
6513
6409
|
//#region src/expression/definitions/global_state.ts
|
|
6514
6410
|
var GlobalState = class GlobalState {
|
|
6515
6411
|
constructor(key) {
|
|
6516
6412
|
this.key = key;
|
|
6517
6413
|
this.type = ValueType;
|
|
6518
6414
|
}
|
|
6519
|
-
static parse(args, context) {
|
|
6520
|
-
if (args.length !== 2) return context.error(`Expected 1 argument, but found ${args.length - 1} instead.`);
|
|
6521
|
-
const key = args[1];
|
|
6522
|
-
if (key === void 0 || key === null) return context.error("Global state property must be defined.");
|
|
6523
|
-
if (typeof key !== "string") return context.error(`Global state property must be string, but found ${typeof args[1]} instead.`);
|
|
6524
|
-
return new GlobalState(key);
|
|
6415
|
+
static parse(args, context) {
|
|
6416
|
+
if (args.length !== 2) return context.error(`Expected 1 argument, but found ${args.length - 1} instead.`);
|
|
6417
|
+
const key = args[1];
|
|
6418
|
+
if (key === void 0 || key === null) return context.error("Global state property must be defined.");
|
|
6419
|
+
if (typeof key !== "string") return context.error(`Global state property must be string, but found ${typeof args[1]} instead.`);
|
|
6420
|
+
return new GlobalState(key);
|
|
6421
|
+
}
|
|
6422
|
+
evaluate(ctx) {
|
|
6423
|
+
const globalState = ctx.globals?.globalState;
|
|
6424
|
+
if (!globalState || Object.keys(globalState).length === 0) return null;
|
|
6425
|
+
return getOwn(globalState, this.key) ?? null;
|
|
6426
|
+
}
|
|
6427
|
+
eachChild() {}
|
|
6428
|
+
outputDefined() {
|
|
6429
|
+
return false;
|
|
6430
|
+
}
|
|
6431
|
+
};
|
|
6432
|
+
|
|
6433
|
+
//#region src/expression/definitions/index.ts
|
|
6434
|
+
const expressions = {
|
|
6435
|
+
"==": Equals,
|
|
6436
|
+
"!=": NotEquals,
|
|
6437
|
+
">": GreaterThan,
|
|
6438
|
+
"<": LessThan,
|
|
6439
|
+
">=": GreaterThanOrEqual,
|
|
6440
|
+
"<=": LessThanOrEqual,
|
|
6441
|
+
array: Assertion,
|
|
6442
|
+
at: At,
|
|
6443
|
+
boolean: Assertion,
|
|
6444
|
+
case: Case,
|
|
6445
|
+
coalesce: Coalesce,
|
|
6446
|
+
collator: CollatorExpression,
|
|
6447
|
+
format: FormatExpression,
|
|
6448
|
+
image: ImageExpression,
|
|
6449
|
+
in: In,
|
|
6450
|
+
"index-of": IndexOf,
|
|
6451
|
+
interpolate: Interpolate,
|
|
6452
|
+
"interpolate-hcl": Interpolate,
|
|
6453
|
+
"interpolate-lab": Interpolate,
|
|
6454
|
+
length: Length,
|
|
6455
|
+
let: Let,
|
|
6456
|
+
literal: Literal,
|
|
6457
|
+
match: Match,
|
|
6458
|
+
number: Assertion,
|
|
6459
|
+
"number-format": NumberFormat,
|
|
6460
|
+
object: Assertion,
|
|
6461
|
+
semiliteral: Semiliteral,
|
|
6462
|
+
slice: Slice,
|
|
6463
|
+
step: Step,
|
|
6464
|
+
string: Assertion,
|
|
6465
|
+
"to-boolean": Coercion,
|
|
6466
|
+
"to-color": Coercion,
|
|
6467
|
+
"to-number": Coercion,
|
|
6468
|
+
"to-string": Coercion,
|
|
6469
|
+
var: Var,
|
|
6470
|
+
within: Within,
|
|
6471
|
+
distance: Distance,
|
|
6472
|
+
"global-state": GlobalState
|
|
6473
|
+
};
|
|
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);
|
|
6525
6605
|
}
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
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));
|
|
6530
6616
|
}
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
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;
|
|
6534
6628
|
}
|
|
6535
6629
|
};
|
|
6536
|
-
|
|
6537
|
-
//#region src/expression/definitions/index.ts
|
|
6538
|
-
const expressions = {
|
|
6539
|
-
"==": Equals,
|
|
6540
|
-
"!=": NotEquals,
|
|
6541
|
-
">": GreaterThan,
|
|
6542
|
-
"<": LessThan,
|
|
6543
|
-
">=": GreaterThanOrEqual,
|
|
6544
|
-
"<=": LessThanOrEqual,
|
|
6545
|
-
array: Assertion,
|
|
6546
|
-
at: At,
|
|
6547
|
-
boolean: Assertion,
|
|
6548
|
-
case: Case,
|
|
6549
|
-
coalesce: Coalesce,
|
|
6550
|
-
collator: CollatorExpression,
|
|
6551
|
-
format: FormatExpression,
|
|
6552
|
-
image: ImageExpression,
|
|
6553
|
-
in: In,
|
|
6554
|
-
"index-of": IndexOf,
|
|
6555
|
-
interpolate: Interpolate,
|
|
6556
|
-
"interpolate-hcl": Interpolate,
|
|
6557
|
-
"interpolate-lab": Interpolate,
|
|
6558
|
-
length: Length,
|
|
6559
|
-
let: Let,
|
|
6560
|
-
literal: Literal,
|
|
6561
|
-
match: Match,
|
|
6562
|
-
number: Assertion,
|
|
6563
|
-
"number-format": NumberFormat,
|
|
6564
|
-
object: Assertion,
|
|
6565
|
-
slice: Slice,
|
|
6566
|
-
step: Step,
|
|
6567
|
-
string: Assertion,
|
|
6568
|
-
"to-boolean": Coercion,
|
|
6569
|
-
"to-color": Coercion,
|
|
6570
|
-
"to-number": Coercion,
|
|
6571
|
-
"to-string": Coercion,
|
|
6572
|
-
var: Var,
|
|
6573
|
-
within: Within,
|
|
6574
|
-
distance: Distance,
|
|
6575
|
-
"global-state": GlobalState
|
|
6576
|
-
};
|
|
6577
|
-
//#endregion
|
|
6630
|
+
|
|
6578
6631
|
//#region src/expression/compound_expression.ts
|
|
6579
6632
|
var CompoundExpression = class CompoundExpression {
|
|
6580
6633
|
constructor(name, type, evaluate, args, key) {
|
|
@@ -7149,21 +7202,7 @@ function isGlobalPropertyConstant(e, properties) {
|
|
|
7149
7202
|
});
|
|
7150
7203
|
return result;
|
|
7151
7204
|
}
|
|
7152
|
-
|
|
7153
|
-
//#region src/util/result.ts
|
|
7154
|
-
function success(value) {
|
|
7155
|
-
return {
|
|
7156
|
-
result: "success",
|
|
7157
|
-
value
|
|
7158
|
-
};
|
|
7159
|
-
}
|
|
7160
|
-
function error(value) {
|
|
7161
|
-
return {
|
|
7162
|
-
result: "error",
|
|
7163
|
-
value
|
|
7164
|
-
};
|
|
7165
|
-
}
|
|
7166
|
-
//#endregion
|
|
7205
|
+
|
|
7167
7206
|
//#region src/util/properties.ts
|
|
7168
7207
|
function supportsPropertyExpression(spec) {
|
|
7169
7208
|
return spec["property-type"] === "data-driven" || spec["property-type"] === "cross-faded-data-driven";
|
|
@@ -7174,13 +7213,13 @@ function supportsZoomExpression(spec) {
|
|
|
7174
7213
|
function supportsInterpolation(spec) {
|
|
7175
7214
|
return !!spec.expression && spec.expression.interpolated;
|
|
7176
7215
|
}
|
|
7177
|
-
|
|
7216
|
+
|
|
7178
7217
|
//#region src/util/extend.ts
|
|
7179
7218
|
function extendBy(output, ...inputs) {
|
|
7180
7219
|
for (const input of inputs) for (const k in input) output[k] = input[k];
|
|
7181
7220
|
return output;
|
|
7182
7221
|
}
|
|
7183
|
-
|
|
7222
|
+
|
|
7184
7223
|
//#region src/util/get_type.ts
|
|
7185
7224
|
function getType(val) {
|
|
7186
7225
|
if (val instanceof Number) return "number";
|
|
@@ -7190,7 +7229,7 @@ function getType(val) {
|
|
|
7190
7229
|
else if (val === null) return "null";
|
|
7191
7230
|
else return typeof val;
|
|
7192
7231
|
}
|
|
7193
|
-
|
|
7232
|
+
|
|
7194
7233
|
//#region src/function/index.ts
|
|
7195
7234
|
function isFunction(value) {
|
|
7196
7235
|
return typeof value === "object" && value !== null && !Array.isArray(value) && typeOf(value) === ObjectType;
|
|
@@ -7288,21 +7327,21 @@ function createFunction(parameters, propertySpec) {
|
|
|
7288
7327
|
kind: "source",
|
|
7289
7328
|
evaluate(_, feature) {
|
|
7290
7329
|
const value = feature && feature.properties ? feature.properties[parameters.property] : void 0;
|
|
7291
|
-
if (value === void 0) return coalesce
|
|
7330
|
+
if (value === void 0) return coalesce(parameters.default, propertySpec.default);
|
|
7292
7331
|
return innerFun(parameters, propertySpec, value, hashedStops, categoricalKeyType);
|
|
7293
7332
|
}
|
|
7294
7333
|
};
|
|
7295
7334
|
}
|
|
7296
|
-
function coalesce
|
|
7335
|
+
function coalesce(a, b, c) {
|
|
7297
7336
|
if (a !== void 0) return a;
|
|
7298
7337
|
if (b !== void 0) return b;
|
|
7299
7338
|
if (c !== void 0) return c;
|
|
7300
7339
|
}
|
|
7301
7340
|
function evaluateCategoricalFunction(parameters, propertySpec, input, hashedStops, keyType) {
|
|
7302
|
-
return coalesce
|
|
7341
|
+
return coalesce(typeof input === keyType ? hashedStops[input] : void 0, parameters.default, propertySpec.default);
|
|
7303
7342
|
}
|
|
7304
7343
|
function evaluateIntervalFunction(parameters, propertySpec, input) {
|
|
7305
|
-
if (getType(input) !== "number") return coalesce
|
|
7344
|
+
if (getType(input) !== "number") return coalesce(parameters.default, propertySpec.default);
|
|
7306
7345
|
const n = parameters.stops.length;
|
|
7307
7346
|
if (n === 1) return parameters.stops[0][1];
|
|
7308
7347
|
if (input <= parameters.stops[0][0]) return parameters.stops[0][1];
|
|
@@ -7312,13 +7351,13 @@ function evaluateIntervalFunction(parameters, propertySpec, input) {
|
|
|
7312
7351
|
}
|
|
7313
7352
|
function evaluateExponentialFunction(parameters, propertySpec, input) {
|
|
7314
7353
|
const base = parameters.base !== void 0 ? parameters.base : 1;
|
|
7315
|
-
if (getType(input) !== "number") return coalesce
|
|
7354
|
+
if (getType(input) !== "number") return coalesce(parameters.default, propertySpec.default);
|
|
7316
7355
|
const n = parameters.stops.length;
|
|
7317
7356
|
if (n === 1) return parameters.stops[0][1];
|
|
7318
7357
|
if (input <= parameters.stops[0][0]) return parameters.stops[0][1];
|
|
7319
7358
|
if (input >= parameters.stops[n - 1][0]) return parameters.stops[n - 1][1];
|
|
7320
7359
|
const index = findStopLessThanOrEqualTo(parameters.stops.map((stop) => stop[0]), input, "");
|
|
7321
|
-
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]);
|
|
7322
7361
|
const outputLower = parameters.stops[index][1];
|
|
7323
7362
|
const outputUpper = parameters.stops[index + 1][1];
|
|
7324
7363
|
const interp = interpolateFactory[propertySpec.type] || identityFunction;
|
|
@@ -7352,7 +7391,7 @@ function evaluateIdentityFunction(parameters, propertySpec, input) {
|
|
|
7352
7391
|
break;
|
|
7353
7392
|
default: if (getType(input) !== propertySpec.type && (propertySpec.type !== "enum" || !propertySpec.values[input])) input = void 0;
|
|
7354
7393
|
}
|
|
7355
|
-
return coalesce
|
|
7394
|
+
return coalesce(input, parameters.default, propertySpec.default);
|
|
7356
7395
|
}
|
|
7357
7396
|
/**
|
|
7358
7397
|
* Returns a ratio that can be used to interpolate between exponential function
|
|
@@ -7392,14 +7431,28 @@ function evaluateIdentityFunction(parameters, propertySpec, input) {
|
|
|
7392
7431
|
*
|
|
7393
7432
|
* @private
|
|
7394
7433
|
*/
|
|
7395
|
-
function interpolationFactor(input, base, lowerValue, upperValue) {
|
|
7434
|
+
function interpolationFactor$1(input, base, lowerValue, upperValue) {
|
|
7396
7435
|
const difference = upperValue - lowerValue;
|
|
7397
7436
|
const progress = input - lowerValue;
|
|
7398
7437
|
if (difference === 0) return 0;
|
|
7399
7438
|
else if (base === 1) return progress / difference;
|
|
7400
7439
|
else return (Math.pow(base, progress) - 1) / (Math.pow(base, difference) - 1);
|
|
7401
7440
|
}
|
|
7402
|
-
|
|
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
|
+
|
|
7403
7456
|
//#region src/expression/index.ts
|
|
7404
7457
|
var StyleExpression = class {
|
|
7405
7458
|
constructor(expression, rootKey, propertySpec, globalState) {
|
|
@@ -7681,7 +7734,7 @@ function addGlobalState(globals, globalState) {
|
|
|
7681
7734
|
globalState
|
|
7682
7735
|
};
|
|
7683
7736
|
}
|
|
7684
|
-
|
|
7737
|
+
|
|
7685
7738
|
//#region src/feature_filter/index.ts
|
|
7686
7739
|
function classifyChildren(children) {
|
|
7687
7740
|
let sawLegacy = false;
|
|
@@ -7787,12 +7840,10 @@ function findMixedLegacyFilter(filter, path = []) {
|
|
|
7787
7840
|
if (diagnostic) return diagnostic;
|
|
7788
7841
|
break;
|
|
7789
7842
|
}
|
|
7790
|
-
case "case":
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
}
|
|
7795
|
-
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
|
+
}
|
|
7796
7847
|
}
|
|
7797
7848
|
return null;
|
|
7798
7849
|
}
|
|
@@ -7830,7 +7881,7 @@ function featureFilter(filter, rootKey, globalState) {
|
|
|
7830
7881
|
needGeometry: false,
|
|
7831
7882
|
getGlobalStateRefs: () => /* @__PURE__ */ new Set()
|
|
7832
7883
|
};
|
|
7833
|
-
if (!isExpressionFilter(filter)) filter = convertFilter
|
|
7884
|
+
if (!isExpressionFilter(filter)) filter = convertFilter(filter);
|
|
7834
7885
|
else warnAboutMixedLegacyFilter(filter, rootKey);
|
|
7835
7886
|
const compiled = createExpression(filter, rootKey, filterSpec, globalState);
|
|
7836
7887
|
if (compiled.result === "error") throw new Error(compiled.value.map((err) => `${err.key}: ${err.message}`).join(", "));
|
|
@@ -7849,13 +7900,13 @@ function geometryNeeded(filter) {
|
|
|
7849
7900
|
for (let index = 1; index < filter.length; index++) if (geometryNeeded(filter[index])) return true;
|
|
7850
7901
|
return false;
|
|
7851
7902
|
}
|
|
7852
|
-
function convertFilter
|
|
7903
|
+
function convertFilter(filter) {
|
|
7853
7904
|
if (!filter) return true;
|
|
7854
7905
|
const op = filter[0];
|
|
7855
7906
|
if (filter.length <= 1) return op !== "any";
|
|
7856
|
-
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;
|
|
7857
7908
|
}
|
|
7858
|
-
function convertComparisonOp
|
|
7909
|
+
function convertComparisonOp(property, value, op) {
|
|
7859
7910
|
switch (property) {
|
|
7860
7911
|
case "$type": return [`filter-type-${op}`, value];
|
|
7861
7912
|
case "$id": return [`filter-id-${op}`, value];
|
|
@@ -7867,9 +7918,9 @@ function convertComparisonOp$1(property, value, op) {
|
|
|
7867
7918
|
}
|
|
7868
7919
|
}
|
|
7869
7920
|
function convertDisjunctionOp(filters) {
|
|
7870
|
-
return ["any"].concat(filters.map(convertFilter
|
|
7921
|
+
return ["any"].concat(filters.map(convertFilter));
|
|
7871
7922
|
}
|
|
7872
|
-
function convertInOp
|
|
7923
|
+
function convertInOp(property, values) {
|
|
7873
7924
|
if (values.length === 0) return false;
|
|
7874
7925
|
switch (property) {
|
|
7875
7926
|
case "$type": return ["filter-type-in", ["literal", values]];
|
|
@@ -7886,7 +7937,7 @@ function convertInOp$1(property, values) {
|
|
|
7886
7937
|
];
|
|
7887
7938
|
}
|
|
7888
7939
|
}
|
|
7889
|
-
function convertHasOp
|
|
7940
|
+
function convertHasOp(property) {
|
|
7890
7941
|
switch (property) {
|
|
7891
7942
|
case "$type": return true;
|
|
7892
7943
|
case "$id": return ["filter-has-id"];
|
|
@@ -7963,6 +8014,10 @@ class Color {
|
|
|
7963
8014
|
get rgb() {
|
|
7964
8015
|
return `#${d2h(this.values[0])}${d2h(this.values[1])}${d2h(this.values[2])}`;
|
|
7965
8016
|
}
|
|
8017
|
+
/** The red, green and blue components as bytes, for painting pixels directly. */
|
|
8018
|
+
get rgbBytes() {
|
|
8019
|
+
return [this.values[0], this.values[1], this.values[2]];
|
|
8020
|
+
}
|
|
7966
8021
|
get opacity() {
|
|
7967
8022
|
return this.values[3] / 255;
|
|
7968
8023
|
}
|
|
@@ -8181,6 +8236,133 @@ function formatNum(tenths) {
|
|
|
8181
8236
|
return (negative ? '-' : '') + String(whole) + '.' + String(frac);
|
|
8182
8237
|
}
|
|
8183
8238
|
|
|
8239
|
+
/**
|
|
8240
|
+
* Where a symbol sits relative to its point, shared by every backend so the two cannot
|
|
8241
|
+
* drift apart. The text vocabulary is SVG's (`text-anchor` / `dominant-baseline`); the
|
|
8242
|
+
* canvas backend translates it to `textAlign` / `textBaseline`.
|
|
8243
|
+
*/
|
|
8244
|
+
function mapTextAnchor(anchor) {
|
|
8245
|
+
switch (anchor) {
|
|
8246
|
+
case 'left':
|
|
8247
|
+
return ['start', 'central'];
|
|
8248
|
+
case 'right':
|
|
8249
|
+
return ['end', 'central'];
|
|
8250
|
+
case 'top':
|
|
8251
|
+
return ['middle', 'text-before-edge'];
|
|
8252
|
+
case 'bottom':
|
|
8253
|
+
return ['middle', 'text-after-edge'];
|
|
8254
|
+
case 'top-left':
|
|
8255
|
+
return ['start', 'text-before-edge'];
|
|
8256
|
+
case 'top-right':
|
|
8257
|
+
return ['end', 'text-before-edge'];
|
|
8258
|
+
case 'bottom-left':
|
|
8259
|
+
return ['start', 'text-after-edge'];
|
|
8260
|
+
case 'bottom-right':
|
|
8261
|
+
return ['end', 'text-after-edge'];
|
|
8262
|
+
default:
|
|
8263
|
+
return ['middle', 'central'];
|
|
8264
|
+
}
|
|
8265
|
+
}
|
|
8266
|
+
function mapIconAnchor(anchor, w, h) {
|
|
8267
|
+
switch (anchor) {
|
|
8268
|
+
case 'left':
|
|
8269
|
+
return [0, -h / 2];
|
|
8270
|
+
case 'right':
|
|
8271
|
+
return [-w, -h / 2];
|
|
8272
|
+
case 'top':
|
|
8273
|
+
return [-w / 2, 0];
|
|
8274
|
+
case 'bottom':
|
|
8275
|
+
return [-w / 2, -h];
|
|
8276
|
+
case 'top-left':
|
|
8277
|
+
return [0, 0];
|
|
8278
|
+
case 'top-right':
|
|
8279
|
+
return [-w, 0];
|
|
8280
|
+
case 'bottom-left':
|
|
8281
|
+
return [0, -h];
|
|
8282
|
+
case 'bottom-right':
|
|
8283
|
+
return [-w, -h];
|
|
8284
|
+
default:
|
|
8285
|
+
return [-w / 2, -h / 2];
|
|
8286
|
+
}
|
|
8287
|
+
}
|
|
8288
|
+
|
|
8289
|
+
/** Size of a raster tile image in user units, when drawn as a mesh of triangles on the globe. */
|
|
8290
|
+
const RASTER_TILE_UNITS = 256;
|
|
8291
|
+
/** How far (pixels) a raster tile image reaches beyond the tile border in the seam underlay. */
|
|
8292
|
+
const RASTER_TILE_BLEED_PX = 1;
|
|
8293
|
+
/** How far (pixels) each raster triangle is grown beyond its edges to overlap its neighbours. */
|
|
8294
|
+
const RASTER_TRIANGLE_OVERLAP_PX = 0.5;
|
|
8295
|
+
/**
|
|
8296
|
+
* The affine transform [a, b, c, d, e, f] mapping the source triangle (in tile units 0..1,
|
|
8297
|
+
* scaled to RASTER_TILE_UNITS) exactly onto the target triangle, or undefined if either is
|
|
8298
|
+
* degenerate.
|
|
8299
|
+
*/
|
|
8300
|
+
function affineFromTriangles(source, target) {
|
|
8301
|
+
const k = RASTER_TILE_UNITS;
|
|
8302
|
+
const [[u0, v0], [u1, v1], [u2, v2]] = source.map(([u, v]) => [u * k, v * k]);
|
|
8303
|
+
const [[x0, y0], [x1, y1], [x2, y2]] = target;
|
|
8304
|
+
const du1 = u1 - u0;
|
|
8305
|
+
const dv1 = v1 - v0;
|
|
8306
|
+
const du2 = u2 - u0;
|
|
8307
|
+
const dv2 = v2 - v0;
|
|
8308
|
+
const det = du1 * dv2 - du2 * dv1;
|
|
8309
|
+
if (Math.abs(det) < 1e-12)
|
|
8310
|
+
return undefined;
|
|
8311
|
+
const dx1 = x1 - x0;
|
|
8312
|
+
const dx2 = x2 - x0;
|
|
8313
|
+
const dy1 = y1 - y0;
|
|
8314
|
+
const dy2 = y2 - y0;
|
|
8315
|
+
// Screen area below a hundredth of a pixel: nothing to draw.
|
|
8316
|
+
if (Math.abs(dx1 * dy2 - dx2 * dy1) < 0.02)
|
|
8317
|
+
return undefined;
|
|
8318
|
+
const a = (dx1 * dv2 - dx2 * dv1) / det;
|
|
8319
|
+
const c = (du1 * dx2 - du2 * dx1) / det;
|
|
8320
|
+
const b = (dy1 * dv2 - dy2 * dv1) / det;
|
|
8321
|
+
const d = (du1 * dy2 - du2 * dy1) / det;
|
|
8322
|
+
return [a, b, c, d, x0 - a * u0 - c * v0, y0 - b * u0 - d * v0];
|
|
8323
|
+
}
|
|
8324
|
+
function isOnTileBorder(source) {
|
|
8325
|
+
return source.some(([u, v]) => u === 0 || u === 1 || v === 0 || v === 1);
|
|
8326
|
+
}
|
|
8327
|
+
/**
|
|
8328
|
+
* Moves the source corners of a triangle that lie on the tile border inwards, by the
|
|
8329
|
+
* equivalent of RASTER_TILE_BLEED_PX on screen, so that the image edge lies beyond the border.
|
|
8330
|
+
* (Used for the underlay that closes the seams between tiles, see `#drawRasterMeshes`.)
|
|
8331
|
+
*/
|
|
8332
|
+
function bleedAtTileBorder(source, target) {
|
|
8333
|
+
const [[u0, v0], [u1, v1], [u2, v2]] = source;
|
|
8334
|
+
const [[x0, y0], [x1, y1], [x2, y2]] = target;
|
|
8335
|
+
const sourceArea = Math.abs((u1 - u0) * (v2 - v0) - (u2 - u0) * (v1 - v0));
|
|
8336
|
+
const targetArea = Math.abs((x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0));
|
|
8337
|
+
if (sourceArea === 0 || targetArea === 0)
|
|
8338
|
+
return source;
|
|
8339
|
+
// Pixels per tile unit, and the bleed in tile units, kept within the triangle.
|
|
8340
|
+
const scale = Math.sqrt(targetArea / sourceArea);
|
|
8341
|
+
const size = Math.max(Math.abs(u1 - u0), Math.abs(u2 - u0), Math.abs(v1 - v0), Math.abs(v2 - v0));
|
|
8342
|
+
const bleed = Math.min(RASTER_TILE_BLEED_PX / scale, size / 4);
|
|
8343
|
+
const inset = (t) => (t === 0 ? bleed : t === 1 ? 1 - bleed : t);
|
|
8344
|
+
return source.map(([u, v]) => [inset(u), inset(v)]);
|
|
8345
|
+
}
|
|
8346
|
+
/**
|
|
8347
|
+
* Moves every edge of a triangle outwards by `distance`, by scaling it around its incenter.
|
|
8348
|
+
* Growth is limited for very thin triangles, whose corners would otherwise shoot far out.
|
|
8349
|
+
*/
|
|
8350
|
+
function growTriangle(triangle, distance) {
|
|
8351
|
+
const [[x0, y0], [x1, y1], [x2, y2]] = triangle;
|
|
8352
|
+
const a = Math.hypot(x2 - x1, y2 - y1);
|
|
8353
|
+
const b = Math.hypot(x2 - x0, y2 - y0);
|
|
8354
|
+
const c = Math.hypot(x1 - x0, y1 - y0);
|
|
8355
|
+
const perimeter = a + b + c;
|
|
8356
|
+
const area = Math.abs((x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0)) / 2;
|
|
8357
|
+
if (perimeter === 0 || area === 0)
|
|
8358
|
+
return triangle;
|
|
8359
|
+
const inradius = (2 * area) / perimeter;
|
|
8360
|
+
const cx = (a * x0 + b * x1 + c * x2) / perimeter;
|
|
8361
|
+
const cy = (a * y0 + b * y1 + c * y2) / perimeter;
|
|
8362
|
+
const scale = 1 + distance / Math.max(inradius, 2 * distance);
|
|
8363
|
+
return triangle.map(([x, y]) => [cx + (x - cx) * scale, cy + (y - cy) * scale]);
|
|
8364
|
+
}
|
|
8365
|
+
|
|
8184
8366
|
// line-blur approximates MapLibre's edge-feather blur, which keeps an opaque core
|
|
8185
8367
|
// and feathers over ~`blur` px with a hard cutoff. feGaussianBlur alone conserves
|
|
8186
8368
|
// total ink and leaves soft, infinite tails, so we combine three corrections,
|
|
@@ -8202,21 +8384,29 @@ class SVGRenderer {
|
|
|
8202
8384
|
width;
|
|
8203
8385
|
height;
|
|
8204
8386
|
#svg;
|
|
8205
|
-
#backgroundColor;
|
|
8206
8387
|
#spriteSheetDefs = new Map();
|
|
8207
8388
|
#spriteSymbolDefs = new Map();
|
|
8208
8389
|
#sdfFilterDefs = new Map();
|
|
8209
8390
|
#blurFilterDefs = new Map();
|
|
8391
|
+
#rasterDefs = [];
|
|
8392
|
+
#rasterClipCount = 0;
|
|
8393
|
+
#clipCircle;
|
|
8210
8394
|
constructor(opt) {
|
|
8211
8395
|
this.width = opt.width;
|
|
8212
8396
|
this.height = opt.height;
|
|
8213
8397
|
this.#svg = [];
|
|
8214
|
-
|
|
8398
|
+
}
|
|
8399
|
+
setClipCircle(circle) {
|
|
8400
|
+
this.#clipCircle = circle;
|
|
8215
8401
|
}
|
|
8216
8402
|
drawBackgroundFill(style) {
|
|
8403
|
+
// Every background layer paints over what is below it, like any other layer. (A
|
|
8404
|
+
// transparent one, e.g. an empty "slot" layer, must not erase an earlier background.)
|
|
8217
8405
|
const color = new Color(style.color);
|
|
8218
8406
|
color.alpha *= style.opacity;
|
|
8219
|
-
|
|
8407
|
+
if (color.alpha <= 0)
|
|
8408
|
+
return;
|
|
8409
|
+
this.#svg.push(`<rect x="-1" y="-1" width="${(this.width + 2).toFixed(0)}" height="${(this.height + 2).toFixed(0)}" ${fillAttr(color)} />`);
|
|
8220
8410
|
}
|
|
8221
8411
|
drawPolygons(id, features) {
|
|
8222
8412
|
if (features.length === 0)
|
|
@@ -8224,9 +8414,11 @@ class SVGRenderer {
|
|
|
8224
8414
|
// Merge only *consecutive* features with identical attributes, so the paint
|
|
8225
8415
|
// order of overlapping features matches MapLibre's source order. A global
|
|
8226
8416
|
// merge would reorder interleaved features (e.g. data-driven fill colors).
|
|
8227
|
-
//
|
|
8228
|
-
//
|
|
8229
|
-
//
|
|
8417
|
+
// Translucent features are never merged: MapLibre blends each one separately,
|
|
8418
|
+
// so where they overlap, their opacity adds up — within one merged <path> it
|
|
8419
|
+
// would not. Fills and their antialias outlines are collected in one pass but
|
|
8420
|
+
// emitted as two: MapLibre draws every fill first, then every outline, so a
|
|
8421
|
+
// lower feature's outline composites on top of a later overlapping fill.
|
|
8230
8422
|
const fillGroups = [];
|
|
8231
8423
|
const outlineGroups = [];
|
|
8232
8424
|
let currentFillKey;
|
|
@@ -8241,37 +8433,48 @@ class SVGRenderer {
|
|
|
8241
8433
|
// Round each ring once; shared by the fill and the outline pass.
|
|
8242
8434
|
const rings = feature.geometry.map((ring) => ring.map((p) => roundXY(p.x, p.y)));
|
|
8243
8435
|
const color = new Color(style.color);
|
|
8436
|
+
const translucent = style.opacity < 1 || color.alpha < 255;
|
|
8244
8437
|
if (color.alpha > 0) {
|
|
8245
8438
|
const key = color.hex + translate + opacityAttr;
|
|
8246
|
-
if (key !== currentFillKey) {
|
|
8439
|
+
if (translucent || key !== currentFillKey) {
|
|
8247
8440
|
fillGroups.push({ segments: [], attrs: `${fillAttr(color)}${translate}${opacityAttr}` });
|
|
8248
|
-
currentFillKey = key;
|
|
8441
|
+
currentFillKey = translucent ? undefined : key;
|
|
8249
8442
|
}
|
|
8250
8443
|
const group = fillGroups[fillGroups.length - 1];
|
|
8251
8444
|
for (const ring of rings)
|
|
8252
8445
|
group.segments.push(ring);
|
|
8253
8446
|
}
|
|
8254
|
-
// fill-antialias outline in
|
|
8255
|
-
//
|
|
8256
|
-
//
|
|
8257
|
-
//
|
|
8258
|
-
//
|
|
8259
|
-
//
|
|
8260
|
-
//
|
|
8261
|
-
if (style.antialias && style.outlineColor !== undefined) {
|
|
8262
|
-
const outlineColor = new Color(style.outlineColor);
|
|
8447
|
+
// fill-antialias outline, in fill-outline-color or else the fill color. For an
|
|
8448
|
+
// opaque fill, MapLibre's default outline IS its fill-edge antialiasing
|
|
8449
|
+
// (draw_fill.ts keeps it outside of the shape), and an SVG <path fill> is already
|
|
8450
|
+
// rasterizer-antialiased, so redrawing it is redundant: measured identical pixel
|
|
8451
|
+
// diff for +46–88% SVG size. It is drawn when it shows: in a distinct
|
|
8452
|
+
// fill-outline-color (choropleths), or for a translucent fill, where MapLibre
|
|
8453
|
+
// draws the outline over the fill's edge (e.g. buildings fading in).
|
|
8454
|
+
if (style.antialias && (style.outlineColor !== undefined || translucent)) {
|
|
8455
|
+
const outlineColor = style.outlineColor !== undefined ? new Color(style.outlineColor) : color;
|
|
8263
8456
|
if (outlineColor.alpha > 0) {
|
|
8457
|
+
const outlineTranslucent = translucent || outlineColor.alpha < 255;
|
|
8264
8458
|
const key = outlineColor.hex + translate + opacityAttr;
|
|
8265
|
-
if (key !== currentOutlineKey) {
|
|
8459
|
+
if (outlineTranslucent || key !== currentOutlineKey) {
|
|
8266
8460
|
outlineGroups.push({
|
|
8267
|
-
|
|
8461
|
+
closed: [],
|
|
8462
|
+
open: [],
|
|
8268
8463
|
attrs: `fill="none" ${strokeAttr(outlineColor, formatScaled(FILL_OUTLINE_WIDTH_PX))}${translate}${opacityAttr}`,
|
|
8269
8464
|
});
|
|
8270
|
-
currentOutlineKey = key;
|
|
8465
|
+
currentOutlineKey = outlineTranslucent ? undefined : key;
|
|
8271
8466
|
}
|
|
8272
8467
|
const group = outlineGroups[outlineGroups.length - 1];
|
|
8273
|
-
|
|
8274
|
-
|
|
8468
|
+
// A polygon clipped to its tile has an outline without the clipped edges.
|
|
8469
|
+
if (feature.outline) {
|
|
8470
|
+
for (const line of feature.outline) {
|
|
8471
|
+
group.open.push(line.map((p) => roundXY(p.x, p.y)));
|
|
8472
|
+
}
|
|
8473
|
+
}
|
|
8474
|
+
else {
|
|
8475
|
+
for (const ring of rings)
|
|
8476
|
+
group.closed.push(ring);
|
|
8477
|
+
}
|
|
8275
8478
|
}
|
|
8276
8479
|
}
|
|
8277
8480
|
});
|
|
@@ -8279,8 +8482,9 @@ class SVGRenderer {
|
|
|
8279
8482
|
for (const { segments, attrs } of fillGroups) {
|
|
8280
8483
|
this.#svg.push(`<path d="${segmentsToPath(segments, true)}" ${attrs} />`);
|
|
8281
8484
|
}
|
|
8282
|
-
for (const {
|
|
8283
|
-
|
|
8485
|
+
for (const { closed, open, attrs } of outlineGroups) {
|
|
8486
|
+
const d = segmentsToPath(closed, true) + segmentsToPath(open, false);
|
|
8487
|
+
this.#svg.push(`<path d="${d}" ${attrs} />`);
|
|
8284
8488
|
}
|
|
8285
8489
|
this.#svg.push('</g>');
|
|
8286
8490
|
}
|
|
@@ -8288,7 +8492,12 @@ class SVGRenderer {
|
|
|
8288
8492
|
if (features.length === 0)
|
|
8289
8493
|
return;
|
|
8290
8494
|
// Merge only *consecutive* same-attribute features (see drawPolygons) so the
|
|
8291
|
-
// paint order of overlapping lines matches MapLibre's source order.
|
|
8495
|
+
// paint order of overlapping lines matches MapLibre's source order. Translucent
|
|
8496
|
+
// lines are never merged: MapLibre blends every line separately, so where lines
|
|
8497
|
+
// overlap (e.g. parallel rail tracks fading in), their opacity adds up — even
|
|
8498
|
+
// between the parts of one feature (tiles merge equal ways into one multi-line
|
|
8499
|
+
// feature). One <path> covers its overlaps only once, so a translucent line gets
|
|
8500
|
+
// a <path> per part.
|
|
8292
8501
|
const groups = [];
|
|
8293
8502
|
let currentKey;
|
|
8294
8503
|
features.forEach(([feature, style]) => {
|
|
@@ -8323,7 +8532,8 @@ class SVGRenderer {
|
|
|
8323
8532
|
translate,
|
|
8324
8533
|
filterAttr,
|
|
8325
8534
|
].join('\0');
|
|
8326
|
-
|
|
8535
|
+
const translucent = effectiveOpacity < 1 || color.alpha < 255;
|
|
8536
|
+
if (translucent || key !== currentKey) {
|
|
8327
8537
|
const attrs = [
|
|
8328
8538
|
'fill="none"',
|
|
8329
8539
|
strokeAttr(color, roundedWidth),
|
|
@@ -8336,8 +8546,9 @@ class SVGRenderer {
|
|
|
8336
8546
|
groups.push({
|
|
8337
8547
|
segments: [],
|
|
8338
8548
|
attrs: attrs.join(' ') + translate + opacityAttr + filterAttr,
|
|
8549
|
+
separate: translucent,
|
|
8339
8550
|
});
|
|
8340
|
-
currentKey = key;
|
|
8551
|
+
currentKey = translucent ? undefined : key;
|
|
8341
8552
|
}
|
|
8342
8553
|
const group = groups[groups.length - 1];
|
|
8343
8554
|
feature.geometry.forEach((line) => {
|
|
@@ -8346,7 +8557,13 @@ class SVGRenderer {
|
|
|
8346
8557
|
});
|
|
8347
8558
|
});
|
|
8348
8559
|
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
8349
|
-
for (const { segments, attrs } of groups) {
|
|
8560
|
+
for (const { segments, attrs, separate } of groups) {
|
|
8561
|
+
if (separate) {
|
|
8562
|
+
for (const segment of segments) {
|
|
8563
|
+
this.#svg.push(`<path d="${segmentsToPath([segment])}" ${attrs} />`);
|
|
8564
|
+
}
|
|
8565
|
+
continue;
|
|
8566
|
+
}
|
|
8350
8567
|
const chains = chainSegments(segments);
|
|
8351
8568
|
const d = segmentsToPath(chains);
|
|
8352
8569
|
this.#svg.push(`<path d="${d}" ${attrs} />`);
|
|
@@ -8603,20 +8820,77 @@ class SVGRenderer {
|
|
|
8603
8820
|
gAttrs += ` filter="${filters.join(' ')}"`;
|
|
8604
8821
|
this.#svg.push(`<g ${gAttrs}>`);
|
|
8605
8822
|
const pixelated = style.resampling === 'nearest';
|
|
8823
|
+
this.#drawRasterMeshes(tiles, pixelated);
|
|
8606
8824
|
for (const tile of tiles) {
|
|
8825
|
+
if (tile.triangles)
|
|
8826
|
+
continue;
|
|
8607
8827
|
const overlap = Math.min(tile.width, tile.height) / 10000; // slight overlap to prevent sub-pixel gaps between tiles
|
|
8608
8828
|
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}"`;
|
|
8609
8829
|
if (pixelated)
|
|
8610
8830
|
attrs += ' style="image-rendering:pixelated"';
|
|
8611
8831
|
this.#svg.push(`<image ${attrs} />`);
|
|
8612
8832
|
}
|
|
8613
|
-
this.#svg.push('</g>');
|
|
8833
|
+
this.#svg.push('</g>');
|
|
8834
|
+
}
|
|
8835
|
+
/**
|
|
8836
|
+
* Draws raster tiles given as meshes of triangles (globe projection). Every tile image is
|
|
8837
|
+
* defined once (as a square of RASTER_TILE_UNITS); every triangle shows it through the affine
|
|
8838
|
+
* transform that maps its three source corners exactly onto its three screen corners, clipped
|
|
8839
|
+
* to the triangle on screen.
|
|
8840
|
+
*
|
|
8841
|
+
* Seams: within a tile, each clip triangle is grown by RASTER_TRIANGLE_OVERLAP_PX; the same
|
|
8842
|
+
* transform continues there, so the overlap shows (almost) the same pixels as the neighbour.
|
|
8843
|
+
* At tile borders that is not enough, as both images end on the same line and their anti-
|
|
8844
|
+
* aliased edges let a hairline of the background shine through. So first, as an underlay,
|
|
8845
|
+
* the triangles along the tile borders are drawn with their image reaching beyond the border
|
|
8846
|
+
* (see {@link bleedAtTileBorder}); then all triangles are drawn exactly on top of it. The
|
|
8847
|
+
* slightly shifted underlay only shows through the seam.
|
|
8848
|
+
*/
|
|
8849
|
+
#drawRasterMeshes(tiles, pixelated) {
|
|
8850
|
+
const meshes = [];
|
|
8851
|
+
for (const tile of tiles) {
|
|
8852
|
+
if (!tile.triangles)
|
|
8853
|
+
continue;
|
|
8854
|
+
const imageId = `raster-${String(this.#rasterDefs.length)}`;
|
|
8855
|
+
const size = String(RASTER_TILE_UNITS);
|
|
8856
|
+
let imageAttrs = `id="${imageId}" width="${size}" height="${size}" preserveAspectRatio="none" xlink:href="${tile.dataUri}"`;
|
|
8857
|
+
if (pixelated)
|
|
8858
|
+
imageAttrs += ' style="image-rendering:pixelated"';
|
|
8859
|
+
this.#rasterDefs.push(`<image ${imageAttrs} />`);
|
|
8860
|
+
meshes.push({ imageId, triangles: tile.triangles });
|
|
8861
|
+
}
|
|
8862
|
+
for (const { imageId, triangles } of meshes) {
|
|
8863
|
+
for (const { source, target } of triangles) {
|
|
8864
|
+
if (!isOnTileBorder(source))
|
|
8865
|
+
continue;
|
|
8866
|
+
this.#drawRasterTriangle(imageId, bleedAtTileBorder(source, target), target);
|
|
8867
|
+
}
|
|
8868
|
+
}
|
|
8869
|
+
for (const { imageId, triangles } of meshes) {
|
|
8870
|
+
for (const { source, target } of triangles) {
|
|
8871
|
+
this.#drawRasterTriangle(imageId, source, target);
|
|
8872
|
+
}
|
|
8873
|
+
}
|
|
8874
|
+
}
|
|
8875
|
+
#drawRasterTriangle(imageId, source, target) {
|
|
8876
|
+
const matrix = affineFromTriangles(source, target);
|
|
8877
|
+
if (!matrix)
|
|
8878
|
+
return;
|
|
8879
|
+
const clip = growTriangle(target, RASTER_TRIANGLE_OVERLAP_PX);
|
|
8880
|
+
const clipId = `raster-clip-${String(this.#rasterClipCount++)}`;
|
|
8881
|
+
const d = segmentsToPath([clip.map(([x, y]) => roundXY(x, y))], true);
|
|
8882
|
+
this.#svg.push(`<clipPath id="${clipId}"><path d="${d}"/></clipPath>` +
|
|
8883
|
+
`<g clip-path="url(#${clipId})"><use xlink:href="#${imageId}" transform="matrix(${matrix.map(formatUnit).join(',')})" /></g>`);
|
|
8614
8884
|
}
|
|
8615
8885
|
getString() {
|
|
8616
8886
|
const w = this.width.toFixed(0);
|
|
8617
8887
|
const h = this.height.toFixed(0);
|
|
8618
8888
|
// Build defs content
|
|
8619
|
-
const
|
|
8889
|
+
const clip = this.#clipCircle;
|
|
8890
|
+
const clipShape = clip
|
|
8891
|
+
? `<circle cx="${formatUnit(clip.x)}" cy="${formatUnit(clip.y)}" r="${formatUnit(clip.radius)}"/>`
|
|
8892
|
+
: `<rect width="${w}" height="${h}"/>`;
|
|
8893
|
+
const defsContent = [`<clipPath id="vb">${clipShape}</clipPath>`, ...this.#rasterDefs];
|
|
8620
8894
|
for (const sheet of this.#spriteSheetDefs.values()) {
|
|
8621
8895
|
defsContent.push(`<image id="${escapeXml(sheet.defId)}" width="${formatNum(sheet.width)}" height="${formatNum(sheet.height)}" xlink:href="${escapeXml(sheet.href)}" />`);
|
|
8622
8896
|
}
|
|
@@ -8643,9 +8917,6 @@ class SVGRenderer {
|
|
|
8643
8917
|
`<defs>\n ${defsContent.join('\n ')}\n</defs>`,
|
|
8644
8918
|
`<g id="map" clip-path="url(#vb)">`,
|
|
8645
8919
|
];
|
|
8646
|
-
if (this.#backgroundColor.alpha > 0) {
|
|
8647
|
-
parts.push(`<rect x="-1" y="-1" width="${(this.width + 2).toFixed(0)}" height="${(this.height + 2).toFixed(0)}" ${fillAttr(this.#backgroundColor)} />`);
|
|
8648
|
-
}
|
|
8649
8920
|
parts.push(...this.#svg, '</g>', '</svg>');
|
|
8650
8921
|
return parts.join('\n');
|
|
8651
8922
|
}
|
|
@@ -8665,6 +8936,9 @@ function strokeAttr(color, width) {
|
|
|
8665
8936
|
function formatScaled(v) {
|
|
8666
8937
|
return formatNum(Math.round(v * 10));
|
|
8667
8938
|
}
|
|
8939
|
+
function formatUnit(v) {
|
|
8940
|
+
return (Math.round(v * 100000) / 100000).toString();
|
|
8941
|
+
}
|
|
8668
8942
|
function formatScale(v) {
|
|
8669
8943
|
return (Math.round(v * 10000) / 10000).toString();
|
|
8670
8944
|
}
|
|
@@ -8675,50 +8949,6 @@ function formatPoint(p) {
|
|
|
8675
8949
|
const [x, y] = roundXY(p[0], p[1]);
|
|
8676
8950
|
return formatNum(x) + ',' + formatNum(y);
|
|
8677
8951
|
}
|
|
8678
|
-
function mapTextAnchor(anchor) {
|
|
8679
|
-
switch (anchor) {
|
|
8680
|
-
case 'left':
|
|
8681
|
-
return ['start', 'central'];
|
|
8682
|
-
case 'right':
|
|
8683
|
-
return ['end', 'central'];
|
|
8684
|
-
case 'top':
|
|
8685
|
-
return ['middle', 'text-before-edge'];
|
|
8686
|
-
case 'bottom':
|
|
8687
|
-
return ['middle', 'text-after-edge'];
|
|
8688
|
-
case 'top-left':
|
|
8689
|
-
return ['start', 'text-before-edge'];
|
|
8690
|
-
case 'top-right':
|
|
8691
|
-
return ['end', 'text-before-edge'];
|
|
8692
|
-
case 'bottom-left':
|
|
8693
|
-
return ['start', 'text-after-edge'];
|
|
8694
|
-
case 'bottom-right':
|
|
8695
|
-
return ['end', 'text-after-edge'];
|
|
8696
|
-
default:
|
|
8697
|
-
return ['middle', 'central'];
|
|
8698
|
-
}
|
|
8699
|
-
}
|
|
8700
|
-
function mapIconAnchor(anchor, w, h) {
|
|
8701
|
-
switch (anchor) {
|
|
8702
|
-
case 'left':
|
|
8703
|
-
return [0, -h / 2];
|
|
8704
|
-
case 'right':
|
|
8705
|
-
return [-w, -h / 2];
|
|
8706
|
-
case 'top':
|
|
8707
|
-
return [-w / 2, 0];
|
|
8708
|
-
case 'bottom':
|
|
8709
|
-
return [-w / 2, -h];
|
|
8710
|
-
case 'top-left':
|
|
8711
|
-
return [0, 0];
|
|
8712
|
-
case 'top-right':
|
|
8713
|
-
return [-w, 0];
|
|
8714
|
-
case 'bottom-left':
|
|
8715
|
-
return [0, -h];
|
|
8716
|
-
case 'bottom-right':
|
|
8717
|
-
return [-w, -h];
|
|
8718
|
-
default:
|
|
8719
|
-
return [-w / 2, -h / 2];
|
|
8720
|
-
}
|
|
8721
|
-
}
|
|
8722
8952
|
function escapeXml(s) {
|
|
8723
8953
|
return s
|
|
8724
8954
|
.replace(/&/g, '&')
|
|
@@ -8758,6 +8988,12 @@ class Feature {
|
|
|
8758
8988
|
properties;
|
|
8759
8989
|
patterns;
|
|
8760
8990
|
geometry;
|
|
8991
|
+
/**
|
|
8992
|
+
* The boundary of a polygon for the fill-antialias outline, as open polylines — when it
|
|
8993
|
+
* differs from the polygon's rings, i.e. when the polygon was clipped to its tile: the
|
|
8994
|
+
* clipped edges along the tile border are not part of the outline.
|
|
8995
|
+
*/
|
|
8996
|
+
outline;
|
|
8761
8997
|
#bbox;
|
|
8762
8998
|
constructor(opt) {
|
|
8763
8999
|
this.type = opt.type;
|
|
@@ -8765,6 +9001,7 @@ class Feature {
|
|
|
8765
9001
|
this.properties = opt.properties;
|
|
8766
9002
|
this.patterns = opt.patterns;
|
|
8767
9003
|
this.geometry = opt.geometry;
|
|
9004
|
+
this.outline = opt.outline;
|
|
8768
9005
|
}
|
|
8769
9006
|
getBbox() {
|
|
8770
9007
|
if (this.#bbox)
|
|
@@ -9859,6 +10096,138 @@ function readUtf8(buf, pos, end) {
|
|
|
9859
10096
|
return str;
|
|
9860
10097
|
}
|
|
9861
10098
|
|
|
10099
|
+
/**
|
|
10100
|
+
* Clipping of vector tile geometry to the tile's own square.
|
|
10101
|
+
*
|
|
10102
|
+
* Vector tiles contain a buffer: geometry reaches a little beyond the tile, so a polygon on
|
|
10103
|
+
* a tile border exists in both tiles. MapLibre draws each tile clipped to its square (with the
|
|
10104
|
+
* stencil buffer), so nothing is drawn twice. Opaque fills hide the difference, translucent
|
|
10105
|
+
* ones do not: the overlap would be drawn with double opacity.
|
|
10106
|
+
*/
|
|
10107
|
+
/** Whether any point lies outside of the square [min, max]². */
|
|
10108
|
+
function exceedsSquare(rings, min, max) {
|
|
10109
|
+
return rings.some((ring) => ring.some(({ x, y }) => x < min || x > max || y < min || y > max));
|
|
10110
|
+
}
|
|
10111
|
+
/**
|
|
10112
|
+
* Clips the rings of a polygon to the square [min, max]² (Sutherland–Hodgman). Rings that
|
|
10113
|
+
* vanish are dropped.
|
|
10114
|
+
*/
|
|
10115
|
+
function clipPolygon(rings, min, max) {
|
|
10116
|
+
const result = [];
|
|
10117
|
+
for (const ring of rings) {
|
|
10118
|
+
let points = ring;
|
|
10119
|
+
points = clipAgainst(points, (p) => p.x >= min, (a, b) => lerpAtX(a, b, min));
|
|
10120
|
+
points = clipAgainst(points, (p) => p.x <= max, (a, b) => lerpAtX(a, b, max));
|
|
10121
|
+
points = clipAgainst(points, (p) => p.y >= min, (a, b) => lerpAtY(a, b, min));
|
|
10122
|
+
points = clipAgainst(points, (p) => p.y <= max, (a, b) => lerpAtY(a, b, max));
|
|
10123
|
+
if (points.length >= 3)
|
|
10124
|
+
result.push(points);
|
|
10125
|
+
}
|
|
10126
|
+
return result;
|
|
10127
|
+
}
|
|
10128
|
+
/**
|
|
10129
|
+
* The boundary of a polygon within the square [min, max]², as open polylines: the parts of
|
|
10130
|
+
* its rings inside the square, without the edges the clipping adds along the square's border.
|
|
10131
|
+
* (MapLibre's fill outline follows the original rings, cut off at the tile border.)
|
|
10132
|
+
*/
|
|
10133
|
+
function clipPolygonOutline(rings, min, max) {
|
|
10134
|
+
const result = [];
|
|
10135
|
+
for (const ring of rings) {
|
|
10136
|
+
if (ring.length < 2)
|
|
10137
|
+
continue;
|
|
10138
|
+
const first = ring[0];
|
|
10139
|
+
const last = ring[ring.length - 1];
|
|
10140
|
+
const closed = first.x === last.x && first.y === last.y ? ring : [...ring, first];
|
|
10141
|
+
result.push(...clipLine(closed, min, max));
|
|
10142
|
+
}
|
|
10143
|
+
return result;
|
|
10144
|
+
}
|
|
10145
|
+
/** Clips a polyline to the square [min, max]², returning the parts inside of it. */
|
|
10146
|
+
function clipLine(line, min, max) {
|
|
10147
|
+
const parts = [];
|
|
10148
|
+
let current = [];
|
|
10149
|
+
for (let i = 0; i + 1 < line.length; i++) {
|
|
10150
|
+
const segment = clipSegment(line[i], line[i + 1], min, max);
|
|
10151
|
+
if (!segment) {
|
|
10152
|
+
if (current.length > 1)
|
|
10153
|
+
parts.push(current);
|
|
10154
|
+
current = [];
|
|
10155
|
+
continue;
|
|
10156
|
+
}
|
|
10157
|
+
const [a, b] = segment;
|
|
10158
|
+
const last = current[current.length - 1];
|
|
10159
|
+
if (last?.x !== a.x || last.y !== a.y) {
|
|
10160
|
+
if (current.length > 1)
|
|
10161
|
+
parts.push(current);
|
|
10162
|
+
current = [a];
|
|
10163
|
+
}
|
|
10164
|
+
current.push(b);
|
|
10165
|
+
// The segment left the square: the line continues elsewhere.
|
|
10166
|
+
if (b !== line[i + 1]) {
|
|
10167
|
+
parts.push(current);
|
|
10168
|
+
current = [];
|
|
10169
|
+
}
|
|
10170
|
+
}
|
|
10171
|
+
if (current.length > 1)
|
|
10172
|
+
parts.push(current);
|
|
10173
|
+
return parts;
|
|
10174
|
+
}
|
|
10175
|
+
/** Liang–Barsky: the part of the segment a→b inside the square, or undefined. */
|
|
10176
|
+
function clipSegment(a, b, min, max) {
|
|
10177
|
+
const dx = b.x - a.x;
|
|
10178
|
+
const dy = b.y - a.y;
|
|
10179
|
+
let t0 = 0;
|
|
10180
|
+
let t1 = 1;
|
|
10181
|
+
for (const [p, q] of [
|
|
10182
|
+
[-dx, a.x - min],
|
|
10183
|
+
[dx, max - a.x],
|
|
10184
|
+
[-dy, a.y - min],
|
|
10185
|
+
[dy, max - a.y],
|
|
10186
|
+
]) {
|
|
10187
|
+
if (p === 0) {
|
|
10188
|
+
if (q < 0)
|
|
10189
|
+
return undefined;
|
|
10190
|
+
continue;
|
|
10191
|
+
}
|
|
10192
|
+
const t = q / p;
|
|
10193
|
+
if (p < 0) {
|
|
10194
|
+
if (t > t1)
|
|
10195
|
+
return undefined;
|
|
10196
|
+
if (t > t0)
|
|
10197
|
+
t0 = t;
|
|
10198
|
+
}
|
|
10199
|
+
else {
|
|
10200
|
+
if (t < t0)
|
|
10201
|
+
return undefined;
|
|
10202
|
+
if (t < t1)
|
|
10203
|
+
t1 = t;
|
|
10204
|
+
}
|
|
10205
|
+
}
|
|
10206
|
+
return [
|
|
10207
|
+
t0 === 0 ? a : { x: a.x + dx * t0, y: a.y + dy * t0 },
|
|
10208
|
+
t1 === 1 ? b : { x: a.x + dx * t1, y: a.y + dy * t1 },
|
|
10209
|
+
];
|
|
10210
|
+
}
|
|
10211
|
+
function clipAgainst(points, inside, intersect) {
|
|
10212
|
+
const result = [];
|
|
10213
|
+
for (let i = 0; i < points.length; i++) {
|
|
10214
|
+
const current = points[i];
|
|
10215
|
+
const previous = points[(i + points.length - 1) % points.length];
|
|
10216
|
+
const currentInside = inside(current);
|
|
10217
|
+
if (currentInside !== inside(previous))
|
|
10218
|
+
result.push(intersect(previous, current));
|
|
10219
|
+
if (currentInside)
|
|
10220
|
+
result.push(current);
|
|
10221
|
+
}
|
|
10222
|
+
return result;
|
|
10223
|
+
}
|
|
10224
|
+
function lerpAtX(a, b, x) {
|
|
10225
|
+
return { x, y: a.y + ((b.y - a.y) * (x - a.x)) / (b.x - a.x) };
|
|
10226
|
+
}
|
|
10227
|
+
function lerpAtY(a, b, y) {
|
|
10228
|
+
return { x: a.x + ((b.x - a.x) * (y - a.y)) / (b.y - a.y), y };
|
|
10229
|
+
}
|
|
10230
|
+
|
|
9862
10231
|
const TILE_EXTENT = 4096;
|
|
9863
10232
|
const VTFeatureType = { Unknown: 0, Point: 1, LineString: 2, Polygon: 3 };
|
|
9864
10233
|
async function loadVectorSource(source, job, layerFeatures) {
|
|
@@ -9866,11 +10235,8 @@ async function loadVectorSource(source, job, layerFeatures) {
|
|
|
9866
10235
|
if (!tiles)
|
|
9867
10236
|
return;
|
|
9868
10237
|
const { width, height } = job.renderer;
|
|
9869
|
-
|
|
9870
|
-
|
|
9871
|
-
await Promise.all(tileCoordinates.map(async ({ x, y, offsetX, offsetY }) => {
|
|
9872
|
-
const offset = new Point2D(offsetX, offsetY);
|
|
9873
|
-
const tile = await getTile(tiles[0], zoomLevel, x, y);
|
|
10238
|
+
await Promise.all(getTileProjections(source, job).map(async ({ x, y, z, project, clipToTile }) => {
|
|
10239
|
+
const tile = await getTile(tiles[0], z, x, y);
|
|
9874
10240
|
if (!tile)
|
|
9875
10241
|
return;
|
|
9876
10242
|
const vectorTile = new VectorTile(new PbfReader(tile.buffer));
|
|
@@ -9882,9 +10248,6 @@ async function loadVectorSource(source, job, layerFeatures) {
|
|
|
9882
10248
|
}
|
|
9883
10249
|
for (let i = 0; i < layer.length; i++) {
|
|
9884
10250
|
const featureSrc = layer.feature(i);
|
|
9885
|
-
const geometry = featureSrc
|
|
9886
|
-
.loadGeometry()
|
|
9887
|
-
.map((ring) => ring.map((point) => new Point2D(point.x, point.y).scale(tileSize / TILE_EXTENT).translate(offset)));
|
|
9888
10251
|
let type;
|
|
9889
10252
|
let list;
|
|
9890
10253
|
switch (featureSrc.type) {
|
|
@@ -9903,6 +10266,22 @@ async function loadVectorSource(source, job, layerFeatures) {
|
|
|
9903
10266
|
list = features.polygons;
|
|
9904
10267
|
break;
|
|
9905
10268
|
}
|
|
10269
|
+
let rings = featureSrc.loadGeometry();
|
|
10270
|
+
let outline;
|
|
10271
|
+
// Clip polygons and lines to the tile, like MapLibre's stencil clipping: otherwise
|
|
10272
|
+
// the parts in the tile buffer are drawn twice, which shows when translucent.
|
|
10273
|
+
if (clipToTile && type !== 'Point' && exceedsSquare(rings, 0, TILE_EXTENT)) {
|
|
10274
|
+
if (type === 'Polygon') {
|
|
10275
|
+
outline = project('LineString', clipPolygonOutline(rings, 0, TILE_EXTENT));
|
|
10276
|
+
rings = clipPolygon(rings, 0, TILE_EXTENT);
|
|
10277
|
+
}
|
|
10278
|
+
else {
|
|
10279
|
+
rings = rings.flatMap((line) => clipLine(line, 0, TILE_EXTENT));
|
|
10280
|
+
}
|
|
10281
|
+
}
|
|
10282
|
+
const geometry = project(type, rings);
|
|
10283
|
+
if (geometry.length === 0)
|
|
10284
|
+
continue;
|
|
9906
10285
|
// Split MultiPoint into individual Point features
|
|
9907
10286
|
if (type === 'Point' && geometry.length > 1) {
|
|
9908
10287
|
for (const ring of geometry) {
|
|
@@ -9920,6 +10299,7 @@ async function loadVectorSource(source, job, layerFeatures) {
|
|
|
9920
10299
|
const feature = new Feature({
|
|
9921
10300
|
type,
|
|
9922
10301
|
geometry,
|
|
10302
|
+
outline,
|
|
9923
10303
|
id: featureSrc.id,
|
|
9924
10304
|
properties: featureSrc.properties,
|
|
9925
10305
|
});
|
|
@@ -9930,9 +10310,41 @@ async function loadVectorSource(source, job, layerFeatures) {
|
|
|
9930
10310
|
}
|
|
9931
10311
|
}));
|
|
9932
10312
|
}
|
|
10313
|
+
function getTileProjections(source, job) {
|
|
10314
|
+
const { width, height } = job.renderer;
|
|
10315
|
+
const { zoom, center } = job.view;
|
|
10316
|
+
const projection = job.projection;
|
|
10317
|
+
if (projection?.isGlobe) {
|
|
10318
|
+
// Never zoom level 0 on the globe: the left and right edge of its single tile coincide
|
|
10319
|
+
// (the antimeridian), which the horizon clipping cannot tell apart.
|
|
10320
|
+
const z = Math.max(1, Math.min(Math.floor(zoom), source.maxzoom ?? Infinity));
|
|
10321
|
+
const scale = 1 / (TILE_EXTENT * 2 ** z);
|
|
10322
|
+
return projection.coveringTiles(z).map(({ x, y }) => ({
|
|
10323
|
+
x,
|
|
10324
|
+
y,
|
|
10325
|
+
z,
|
|
10326
|
+
clipToTile: projection.isTileFullyVisible({ x, y, z }),
|
|
10327
|
+
project: (type, rings) => projection.projectGeometry(type, rings.map((ring) => ring.map((p) => [
|
|
10328
|
+
(x * TILE_EXTENT + p.x) * scale,
|
|
10329
|
+
(y * TILE_EXTENT + p.y) * scale,
|
|
10330
|
+
]))),
|
|
10331
|
+
}));
|
|
10332
|
+
}
|
|
10333
|
+
const { zoomLevel, tileSize, tiles } = calculateTileGrid(width, height, center, zoom, source.maxzoom);
|
|
10334
|
+
return tiles.map(({ x, y, offsetX, offsetY }) => {
|
|
10335
|
+
const offset = new Point2D(offsetX, offsetY);
|
|
10336
|
+
return {
|
|
10337
|
+
x,
|
|
10338
|
+
y,
|
|
10339
|
+
z: zoomLevel,
|
|
10340
|
+
clipToTile: true,
|
|
10341
|
+
project: (_type, rings) => rings.map((ring) => ring.map((point) => new Point2D(point.x, point.y).scale(tileSize / TILE_EXTENT).translate(offset))),
|
|
10342
|
+
};
|
|
10343
|
+
});
|
|
10344
|
+
}
|
|
9933
10345
|
|
|
9934
10346
|
function loadGeoJSONSource(options) {
|
|
9935
|
-
const { sourceName, data, width, height, zoom, center, layerFeatures } = options;
|
|
10347
|
+
const { sourceName, data, width, height, zoom, center, layerFeatures, projection } = options;
|
|
9936
10348
|
const existing = layerFeatures.get(sourceName);
|
|
9937
10349
|
const features = existing ?? {
|
|
9938
10350
|
points: [],
|
|
@@ -9945,9 +10357,17 @@ function loadGeoJSONSource(options) {
|
|
|
9945
10357
|
layerFeatures.set(sourceName, features);
|
|
9946
10358
|
const worldSize = 512 * 2 ** zoom;
|
|
9947
10359
|
const centerMercator = new Point2D(center[0], center[1]).getProject2Pixel();
|
|
9948
|
-
function
|
|
10360
|
+
function toMercator(coord) {
|
|
9949
10361
|
const mercator = new Point2D(coord[0], coord[1]).getProject2Pixel();
|
|
9950
|
-
return
|
|
10362
|
+
return [mercator.x, mercator.y];
|
|
10363
|
+
}
|
|
10364
|
+
function project(type, rings) {
|
|
10365
|
+
if (projection?.isGlobe) {
|
|
10366
|
+
if (type === 'Polygon')
|
|
10367
|
+
rings = orientRings(rings);
|
|
10368
|
+
return projection.projectGeometry(type, rings);
|
|
10369
|
+
}
|
|
10370
|
+
return rings.map((ring) => ring.map(([x, y]) => new Point2D((x - centerMercator.x) * worldSize + width / 2, (y - centerMercator.y) * worldSize + height / 2)));
|
|
9951
10371
|
}
|
|
9952
10372
|
function makeFeature(type, geometry, id, properties) {
|
|
9953
10373
|
const feature = new Feature({ type, geometry, id, properties });
|
|
@@ -9958,7 +10378,10 @@ function loadGeoJSONSource(options) {
|
|
|
9958
10378
|
function extractPoints(geometry) {
|
|
9959
10379
|
return geometry.flatMap((ring) => ring.map((p) => [p]));
|
|
9960
10380
|
}
|
|
9961
|
-
function addFeature(type,
|
|
10381
|
+
function addFeature(type, rings, id, properties) {
|
|
10382
|
+
const geometry = project(type, rings);
|
|
10383
|
+
if (geometry.length === 0)
|
|
10384
|
+
return;
|
|
9962
10385
|
switch (type) {
|
|
9963
10386
|
case 'Point': {
|
|
9964
10387
|
const f = makeFeature('Point', geometry, id, properties);
|
|
@@ -10001,23 +10424,23 @@ function loadGeoJSONSource(options) {
|
|
|
10001
10424
|
function processGeometry(geom, id, properties) {
|
|
10002
10425
|
switch (geom.type) {
|
|
10003
10426
|
case 'Point':
|
|
10004
|
-
addFeature('Point', [[
|
|
10427
|
+
addFeature('Point', [[toMercator(geom.coordinates)]], id, properties);
|
|
10005
10428
|
break;
|
|
10006
10429
|
case 'MultiPoint':
|
|
10007
|
-
addFeature('Point', geom.coordinates.map((c) => [
|
|
10430
|
+
addFeature('Point', geom.coordinates.map((c) => [toMercator(c)]), id, properties);
|
|
10008
10431
|
break;
|
|
10009
10432
|
case 'LineString':
|
|
10010
|
-
addFeature('LineString', [geom.coordinates.map((c) =>
|
|
10433
|
+
addFeature('LineString', [geom.coordinates.map((c) => toMercator(c))], id, properties);
|
|
10011
10434
|
break;
|
|
10012
10435
|
case 'MultiLineString':
|
|
10013
|
-
addFeature('LineString', geom.coordinates.map((line) => line.map((c) =>
|
|
10436
|
+
addFeature('LineString', geom.coordinates.map((line) => line.map((c) => toMercator(c))), id, properties);
|
|
10014
10437
|
break;
|
|
10015
10438
|
case 'Polygon':
|
|
10016
|
-
addFeature('Polygon', geom.coordinates.map((ring) => ring.map((c) =>
|
|
10439
|
+
addFeature('Polygon', geom.coordinates.map((ring) => ring.map((c) => toMercator(c))), id, properties);
|
|
10017
10440
|
break;
|
|
10018
10441
|
case 'MultiPolygon':
|
|
10019
10442
|
for (const polygon of geom.coordinates) {
|
|
10020
|
-
addFeature('Polygon', polygon.map((ring) => ring.map((c) =>
|
|
10443
|
+
addFeature('Polygon', polygon.map((ring) => ring.map((c) => toMercator(c))), id, properties);
|
|
10021
10444
|
}
|
|
10022
10445
|
break;
|
|
10023
10446
|
case 'GeometryCollection':
|
|
@@ -10041,6 +10464,22 @@ function loadGeoJSONSource(options) {
|
|
|
10041
10464
|
break;
|
|
10042
10465
|
}
|
|
10043
10466
|
}
|
|
10467
|
+
/**
|
|
10468
|
+
* Orients polygon rings like vector tiles do (in mercator space, y pointing south: the
|
|
10469
|
+
* exterior ring clockwise, holes counter-clockwise), as the globe clipping relies on it.
|
|
10470
|
+
*/
|
|
10471
|
+
function orientRings(rings) {
|
|
10472
|
+
return rings.map((ring, index) => {
|
|
10473
|
+
let area = 0;
|
|
10474
|
+
for (let i = 0; i < ring.length; i++) {
|
|
10475
|
+
const [x0, y0] = ring[i];
|
|
10476
|
+
const [x1, y1] = ring[(i + 1) % ring.length];
|
|
10477
|
+
area += x0 * y1 - x1 * y0;
|
|
10478
|
+
}
|
|
10479
|
+
const isExterior = index === 0;
|
|
10480
|
+
return area > 0 === isExterior ? ring : [...ring].reverse();
|
|
10481
|
+
});
|
|
10482
|
+
}
|
|
10044
10483
|
|
|
10045
10484
|
/**
|
|
10046
10485
|
* Base64-encode an ArrayBuffer (standard alphabet, with padding).
|
|
@@ -10078,6 +10517,23 @@ async function getRasterTiles(job, sourceName) {
|
|
|
10078
10517
|
throw Error(`Invalid raster source "${sourceName}": expected type "raster" with a "tiles" array`);
|
|
10079
10518
|
}
|
|
10080
10519
|
const sourceUrl = source.tiles[0];
|
|
10520
|
+
const projection = job.projection;
|
|
10521
|
+
if (projection?.isGlobe) {
|
|
10522
|
+
// Never zoom level 0 on the globe: the left and right edge of its single tile coincide
|
|
10523
|
+
// (the antimeridian), which the horizon clipping cannot tell apart.
|
|
10524
|
+
const z = Math.max(1, Math.min(Math.floor(zoom), source.maxzoom ?? Infinity));
|
|
10525
|
+
const globeTiles = await Promise.all(projection.coveringTiles(z).map(async (id) => {
|
|
10526
|
+
const tile = await getTile(sourceUrl, id.z, id.x, id.y);
|
|
10527
|
+
if (!tile)
|
|
10528
|
+
return null;
|
|
10529
|
+
const triangles = projection.rasterTriangles(id);
|
|
10530
|
+
if (triangles.length === 0)
|
|
10531
|
+
return null;
|
|
10532
|
+
const dataUri = `data:${tile.contentType};base64,${arrayBufferToBase64(tile.buffer)}`;
|
|
10533
|
+
return { x: 0, y: 0, width: 1, height: 1, dataUri, triangles };
|
|
10534
|
+
}));
|
|
10535
|
+
return globeTiles.filter((tile) => tile !== null);
|
|
10536
|
+
}
|
|
10081
10537
|
const { zoomLevel, tileSize, tiles } = calculateTileGrid(width, height, center, zoom, source.maxzoom);
|
|
10082
10538
|
const rasterTiles = await Promise.all(tiles.map(async ({ x, y, offsetX, offsetY }) => {
|
|
10083
10539
|
const tile = await getTile(sourceUrl, zoomLevel, x, y);
|
|
@@ -10181,6 +10637,7 @@ async function getLayerFeatures(job) {
|
|
|
10181
10637
|
zoom,
|
|
10182
10638
|
center,
|
|
10183
10639
|
layerFeatures,
|
|
10640
|
+
projection: job.projection,
|
|
10184
10641
|
});
|
|
10185
10642
|
}
|
|
10186
10643
|
break;
|
|
@@ -10312,6 +10769,594 @@ function getLayerStyles(layers) {
|
|
|
10312
10769
|
return layers.map(createStyleLayer);
|
|
10313
10770
|
}
|
|
10314
10771
|
|
|
10772
|
+
/**
|
|
10773
|
+
* Map projection: web mercator, MapLibre's globe ("vertical-perspective") or a blend of both.
|
|
10774
|
+
*
|
|
10775
|
+
* The math follows MapLibre GL JS (`vertical_perspective_transform.ts` and the
|
|
10776
|
+
* `_projection_globe.vertex.glsl` shader) for a camera without pitch, bearing or padding:
|
|
10777
|
+
* the globe is a sphere of radius `worldSize / (2π·cos(centerLat))` (so the map center has
|
|
10778
|
+
* the same scale as in mercator), viewed by a perspective camera `1.5 · height` pixels above
|
|
10779
|
+
* the map center. The globe → mercator transition blends the homogeneous clip-space positions
|
|
10780
|
+
* of both projections (x, y and w) *before* the perspective divide, exactly like the shader.
|
|
10781
|
+
*
|
|
10782
|
+
* All inputs are mercator world coordinates in the range 0..1 (`mx` east, `my` south).
|
|
10783
|
+
*/
|
|
10784
|
+
/** MapLibre's default vertical field of view (`TransformHelper._fovInRadians`). */
|
|
10785
|
+
const FOV = 0.6435011087932844;
|
|
10786
|
+
/** What MapLibre expands `projection: { type: 'globe' }` into (`projection_factory.ts`). */
|
|
10787
|
+
const GLOBE_PRESET = [
|
|
10788
|
+
'interpolate',
|
|
10789
|
+
['linear'],
|
|
10790
|
+
['zoom'],
|
|
10791
|
+
11,
|
|
10792
|
+
'vertical-perspective',
|
|
10793
|
+
12,
|
|
10794
|
+
'mercator',
|
|
10795
|
+
];
|
|
10796
|
+
/** Below this globeness the far side of the globe is not clipped (MapLibre's `z_globeness_threshold`). */
|
|
10797
|
+
const CLIP_THRESHOLD = 0.2;
|
|
10798
|
+
/** Maximum distance (pixels) a straight segment may deviate from the curved globe surface. */
|
|
10799
|
+
const MAX_CURVE_ERROR_PX = 0.25;
|
|
10800
|
+
/** The same for the affine-mapped triangles of raster tiles (a coarser mesh keeps the SVG small). */
|
|
10801
|
+
const MAX_RASTER_ERROR_PX = 1;
|
|
10802
|
+
/** Upper bound for the number of cells per raster tile side. */
|
|
10803
|
+
const MAX_RASTER_CELLS = 16;
|
|
10804
|
+
/** Angle to go clockwise on screen (decreasing angle in the y-up view frame) from a0 to a1, in [0, 2π). */
|
|
10805
|
+
function clockwiseDelta(a0, a1) {
|
|
10806
|
+
const delta = (a0 - a1) % (2 * Math.PI);
|
|
10807
|
+
return delta < 0 ? delta + 2 * Math.PI : delta;
|
|
10808
|
+
}
|
|
10809
|
+
/**
|
|
10810
|
+
* Returns how "globe-like" the projection is at the given zoom, matching MapLibre's
|
|
10811
|
+
* `GlobeProjection.transitionState`: 0 = mercator, 1 = globe, in between = transition.
|
|
10812
|
+
*/
|
|
10813
|
+
function getGlobeness(projection, zoom) {
|
|
10814
|
+
return evaluateGlobeness(projection?.type ?? 'mercator', zoom);
|
|
10815
|
+
}
|
|
10816
|
+
function evaluateGlobeness(value, zoom) {
|
|
10817
|
+
if (typeof value === 'string') {
|
|
10818
|
+
switch (value) {
|
|
10819
|
+
case 'globe':
|
|
10820
|
+
return evaluateGlobeness(GLOBE_PRESET, zoom);
|
|
10821
|
+
case 'vertical-perspective':
|
|
10822
|
+
return 1;
|
|
10823
|
+
default:
|
|
10824
|
+
return 0;
|
|
10825
|
+
}
|
|
10826
|
+
}
|
|
10827
|
+
if (!Array.isArray(value))
|
|
10828
|
+
return 0;
|
|
10829
|
+
const [op, ...args] = value;
|
|
10830
|
+
if (op === 'literal')
|
|
10831
|
+
return evaluateGlobeness(args[0], zoom);
|
|
10832
|
+
if (op === 'step') {
|
|
10833
|
+
// ["step", ["zoom"], output0, stop1, output1, ...]
|
|
10834
|
+
let result = args[1];
|
|
10835
|
+
for (let i = 2; i + 1 < args.length; i += 2) {
|
|
10836
|
+
if (zoom >= args[i])
|
|
10837
|
+
result = args[i + 1];
|
|
10838
|
+
}
|
|
10839
|
+
return evaluateGlobeness(result, zoom);
|
|
10840
|
+
}
|
|
10841
|
+
if (op === 'interpolate') {
|
|
10842
|
+
// ["interpolate", ["linear"] | ["exponential", base], ["zoom"], stop0, output0, ...]
|
|
10843
|
+
const interpolation = args[0];
|
|
10844
|
+
const base = interpolation[0] === 'exponential' ? interpolation[1] : 1;
|
|
10845
|
+
const stops = [];
|
|
10846
|
+
for (let i = 2; i + 1 < args.length; i += 2)
|
|
10847
|
+
stops.push([args[i], args[i + 1]]);
|
|
10848
|
+
if (stops.length === 0)
|
|
10849
|
+
return 0;
|
|
10850
|
+
if (zoom <= stops[0][0])
|
|
10851
|
+
return evaluateGlobeness(stops[0][1], zoom);
|
|
10852
|
+
for (let i = 1; i < stops.length; i++) {
|
|
10853
|
+
const [z1, out1] = stops[i];
|
|
10854
|
+
if (zoom > z1)
|
|
10855
|
+
continue;
|
|
10856
|
+
const [z0, out0] = stops[i - 1];
|
|
10857
|
+
const t = interpolationFactor(zoom, z0, z1, base);
|
|
10858
|
+
const g0 = evaluateGlobeness(out0, zoom);
|
|
10859
|
+
const g1 = evaluateGlobeness(out1, zoom);
|
|
10860
|
+
return g0 + (g1 - g0) * t;
|
|
10861
|
+
}
|
|
10862
|
+
return evaluateGlobeness(stops[stops.length - 1][1], zoom);
|
|
10863
|
+
}
|
|
10864
|
+
// A projection transition literal: [from, to, transition]
|
|
10865
|
+
if (value.length === 3 && typeof value[2] === 'number') {
|
|
10866
|
+
const g0 = evaluateGlobeness(value[0], zoom);
|
|
10867
|
+
const g1 = evaluateGlobeness(value[1], zoom);
|
|
10868
|
+
return g0 + (g1 - g0) * value[2];
|
|
10869
|
+
}
|
|
10870
|
+
return 0;
|
|
10871
|
+
}
|
|
10872
|
+
function interpolationFactor(input, lower, upper, base) {
|
|
10873
|
+
const difference = upper - lower;
|
|
10874
|
+
if (difference === 0)
|
|
10875
|
+
return 0;
|
|
10876
|
+
const progress = input - lower;
|
|
10877
|
+
if (base === 1)
|
|
10878
|
+
return progress / difference;
|
|
10879
|
+
return (Math.pow(base, progress) - 1) / (Math.pow(base, difference) - 1);
|
|
10880
|
+
}
|
|
10881
|
+
class Projection {
|
|
10882
|
+
width;
|
|
10883
|
+
height;
|
|
10884
|
+
zoom;
|
|
10885
|
+
/** 0 = mercator, 1 = globe, in between = transition (see {@link getGlobeness}). */
|
|
10886
|
+
globeness;
|
|
10887
|
+
/** Size of the whole world in pixels at the current zoom (mercator). */
|
|
10888
|
+
worldSize;
|
|
10889
|
+
/**
|
|
10890
|
+
* Maximum length (in mercator units) of a straight segment before it has to be
|
|
10891
|
+
* subdivided to follow the curvature of the globe.
|
|
10892
|
+
*/
|
|
10893
|
+
maxSegmentLength;
|
|
10894
|
+
/** Whether geometry on the far side of the globe has to be removed. */
|
|
10895
|
+
clipsHorizon;
|
|
10896
|
+
#centerX;
|
|
10897
|
+
#centerY;
|
|
10898
|
+
#centerLng;
|
|
10899
|
+
#sinLat;
|
|
10900
|
+
#cosLat;
|
|
10901
|
+
/** Globe radius in pixels. */
|
|
10902
|
+
#radius;
|
|
10903
|
+
/** Distance of the camera to the map center in pixels. */
|
|
10904
|
+
#cameraDistance;
|
|
10905
|
+
/** Points of the unit sphere with a view-space z below this are behind the horizon. */
|
|
10906
|
+
#horizonZ;
|
|
10907
|
+
constructor(opt) {
|
|
10908
|
+
this.width = opt.width;
|
|
10909
|
+
this.height = opt.height;
|
|
10910
|
+
this.zoom = opt.zoom;
|
|
10911
|
+
this.globeness = Math.min(1, Math.max(0, opt.globeness ?? 0));
|
|
10912
|
+
this.worldSize = 512 * 2 ** opt.zoom;
|
|
10913
|
+
const center = new Point2D(opt.center[0], opt.center[1]).getProject2Pixel();
|
|
10914
|
+
this.#centerX = center.x;
|
|
10915
|
+
this.#centerY = center.y;
|
|
10916
|
+
const lat = (opt.center[1] * Math.PI) / 180;
|
|
10917
|
+
this.#centerLng = (opt.center[0] * Math.PI) / 180;
|
|
10918
|
+
this.#sinLat = Math.sin(lat);
|
|
10919
|
+
this.#cosLat = Math.cos(lat);
|
|
10920
|
+
this.#radius = this.worldSize / (2 * Math.PI * this.#cosLat);
|
|
10921
|
+
this.#cameraDistance = this.height / 2 / Math.tan(FOV / 2);
|
|
10922
|
+
this.#horizonZ = this.#radius / (this.#radius + this.#cameraDistance);
|
|
10923
|
+
this.clipsHorizon = this.globeness > CLIP_THRESHOLD;
|
|
10924
|
+
// A chord spanning the angle θ deviates R·θ²/8 from the sphere.
|
|
10925
|
+
const maxAngle = Math.sqrt((8 * MAX_CURVE_ERROR_PX) / this.#radius);
|
|
10926
|
+
this.maxSegmentLength = maxAngle / (2 * Math.PI);
|
|
10927
|
+
}
|
|
10928
|
+
static fromStyle(opt) {
|
|
10929
|
+
return new Projection({ ...opt, globeness: getGlobeness(opt.projection, opt.zoom) });
|
|
10930
|
+
}
|
|
10931
|
+
/** True when the globe projection is (at least partially) in effect. */
|
|
10932
|
+
get isGlobe() {
|
|
10933
|
+
return this.globeness > 0;
|
|
10934
|
+
}
|
|
10935
|
+
/**
|
|
10936
|
+
* The silhouette of the globe on screen, if the map has to be clipped to it.
|
|
10937
|
+
* (During the globe → mercator transition the silhouette is far outside the screen.)
|
|
10938
|
+
*/
|
|
10939
|
+
get clipCircle() {
|
|
10940
|
+
if (!this.clipsHorizon)
|
|
10941
|
+
return undefined;
|
|
10942
|
+
const r = this.#radius;
|
|
10943
|
+
const d = this.#cameraDistance;
|
|
10944
|
+
const radius = (d * r) / Math.sqrt((d + r) * (d + r) - r * r);
|
|
10945
|
+
return { x: this.width / 2, y: this.height / 2, radius };
|
|
10946
|
+
}
|
|
10947
|
+
/** Mercator world coordinates → unit sphere in the view frame (x right, y up, z towards the camera). */
|
|
10948
|
+
toSphere(mx, my) {
|
|
10949
|
+
const dLng = mx * 2 * Math.PI - Math.PI - this.#centerLng;
|
|
10950
|
+
const lat = 2 * Math.atan(Math.exp(Math.PI - my * 2 * Math.PI)) - Math.PI / 2;
|
|
10951
|
+
const cosLat = Math.cos(lat);
|
|
10952
|
+
const x = Math.sin(dLng) * cosLat;
|
|
10953
|
+
const y = Math.sin(lat);
|
|
10954
|
+
const z = Math.cos(dLng) * cosLat;
|
|
10955
|
+
return [x, y * this.#cosLat - z * this.#sinLat, y * this.#sinLat + z * this.#cosLat];
|
|
10956
|
+
}
|
|
10957
|
+
/** Inverse of {@link toSphere}. */
|
|
10958
|
+
fromSphere(v) {
|
|
10959
|
+
const y = v[1] * this.#cosLat + v[2] * this.#sinLat;
|
|
10960
|
+
const z = -v[1] * this.#sinLat + v[2] * this.#cosLat;
|
|
10961
|
+
const lat = Math.asin(Math.max(-1, Math.min(1, y)));
|
|
10962
|
+
const lng = Math.atan2(v[0], z) + this.#centerLng;
|
|
10963
|
+
const mx = (lng + Math.PI) / (2 * Math.PI);
|
|
10964
|
+
const my = (Math.PI - Math.log(Math.tan(Math.PI / 4 + lat / 2))) / (2 * Math.PI);
|
|
10965
|
+
return [mx - Math.floor(mx), my];
|
|
10966
|
+
}
|
|
10967
|
+
isVisible(v) {
|
|
10968
|
+
return !this.clipsHorizon || v[2] >= this.#horizonZ;
|
|
10969
|
+
}
|
|
10970
|
+
/** Screen position of a mercator point. */
|
|
10971
|
+
project(mx, my, v) {
|
|
10972
|
+
// Flat (mercator) position, relative to the screen center. Use the world copy
|
|
10973
|
+
// nearest to the center.
|
|
10974
|
+
let dx = mx - this.#centerX;
|
|
10975
|
+
dx -= Math.round(dx);
|
|
10976
|
+
const flatX = dx * this.worldSize;
|
|
10977
|
+
const flatY = (my - this.#centerY) * this.worldSize;
|
|
10978
|
+
const t = this.globeness;
|
|
10979
|
+
if (t === 0)
|
|
10980
|
+
return new Point2D(flatX + this.width / 2, flatY + this.height / 2);
|
|
10981
|
+
v ??= this.toSphere(mx, my);
|
|
10982
|
+
const d = this.#cameraDistance;
|
|
10983
|
+
const r = this.#radius;
|
|
10984
|
+
// Homogeneous coordinates (x, y, w) of the globe position; the flat one is (d·x, d·y, d).
|
|
10985
|
+
const globeX = d * r * v[0];
|
|
10986
|
+
const globeY = -d * r * v[1];
|
|
10987
|
+
const globeW = d + r * (1 - v[2]);
|
|
10988
|
+
const w = d + (globeW - d) * t;
|
|
10989
|
+
return new Point2D((d * flatX + (globeX - d * flatX) * t) / w + this.width / 2, (d * flatY + (globeY - d * flatY) * t) / w + this.height / 2);
|
|
10990
|
+
}
|
|
10991
|
+
/**
|
|
10992
|
+
* Projects feature geometry given in mercator world coordinates to screen pixels.
|
|
10993
|
+
* On the globe, segments are subdivided to follow the curvature, lines are cut at
|
|
10994
|
+
* the horizon and polygons are clipped to the visible hemisphere (the clipped part of
|
|
10995
|
+
* the boundary follows the globe's silhouette).
|
|
10996
|
+
*/
|
|
10997
|
+
projectGeometry(type, rings) {
|
|
10998
|
+
if (!this.isGlobe)
|
|
10999
|
+
return rings.map((ring) => ring.map(([mx, my]) => this.project(mx, my)));
|
|
11000
|
+
switch (type) {
|
|
11001
|
+
case 'Point':
|
|
11002
|
+
return rings
|
|
11003
|
+
.map((ring) => ring.flatMap(([mx, my]) => {
|
|
11004
|
+
const v = this.toSphere(mx, my);
|
|
11005
|
+
return this.isVisible(v) ? [this.project(mx, my, v)] : [];
|
|
11006
|
+
}))
|
|
11007
|
+
.filter((ring) => ring.length > 0);
|
|
11008
|
+
case 'LineString':
|
|
11009
|
+
return rings.flatMap((ring) => this.#clipLine(this.#densify(ring, false)));
|
|
11010
|
+
case 'Polygon':
|
|
11011
|
+
return this.#clipPolygon(rings.map((ring) => this.#densify(ring, true)));
|
|
11012
|
+
}
|
|
11013
|
+
}
|
|
11014
|
+
/** Subdivides long segments (in mercator space) and lifts the points onto the sphere. */
|
|
11015
|
+
#densify(ring, closed) {
|
|
11016
|
+
const maxLength = this.maxSegmentLength;
|
|
11017
|
+
const result = [];
|
|
11018
|
+
const count = ring.length;
|
|
11019
|
+
const segments = closed ? count : count - 1;
|
|
11020
|
+
for (let i = 0; i < count; i++) {
|
|
11021
|
+
const [x0, y0] = ring[i];
|
|
11022
|
+
result.push({ mx: x0, my: y0, v: this.toSphere(x0, y0) });
|
|
11023
|
+
if (i >= segments)
|
|
11024
|
+
continue;
|
|
11025
|
+
const [x1, y1] = ring[(i + 1) % count];
|
|
11026
|
+
const steps = Math.ceil(Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0)) / maxLength);
|
|
11027
|
+
for (let s = 1; s < steps; s++) {
|
|
11028
|
+
const mx = x0 + ((x1 - x0) * s) / steps;
|
|
11029
|
+
const my = y0 + ((y1 - y0) * s) / steps;
|
|
11030
|
+
result.push({ mx, my, v: this.toSphere(mx, my) });
|
|
11031
|
+
}
|
|
11032
|
+
}
|
|
11033
|
+
return result;
|
|
11034
|
+
}
|
|
11035
|
+
/** Point on the segment a→b where it crosses the horizon plane. */
|
|
11036
|
+
#horizonCrossing(a, b) {
|
|
11037
|
+
const s = (a.v[2] - this.#horizonZ) / (a.v[2] - b.v[2]);
|
|
11038
|
+
const mx = a.mx + (b.mx - a.mx) * s;
|
|
11039
|
+
const my = a.my + (b.my - a.my) * s;
|
|
11040
|
+
return { mx, my, v: this.toSphere(mx, my) };
|
|
11041
|
+
}
|
|
11042
|
+
/** Splits a line into its visible parts. */
|
|
11043
|
+
#clipLine(points) {
|
|
11044
|
+
if (!this.clipsHorizon)
|
|
11045
|
+
return [points.map((p) => this.project(p.mx, p.my, p.v))];
|
|
11046
|
+
const parts = [];
|
|
11047
|
+
let current = [];
|
|
11048
|
+
for (let i = 0; i < points.length; i++) {
|
|
11049
|
+
const p = points[i];
|
|
11050
|
+
const visible = this.isVisible(p.v);
|
|
11051
|
+
const prev = i > 0 ? points[i - 1] : undefined;
|
|
11052
|
+
if (prev && this.isVisible(prev.v) !== visible) {
|
|
11053
|
+
const c = this.#horizonCrossing(prev, p);
|
|
11054
|
+
current.push(this.project(c.mx, c.my, c.v));
|
|
11055
|
+
if (!visible) {
|
|
11056
|
+
parts.push(current);
|
|
11057
|
+
current = [];
|
|
11058
|
+
}
|
|
11059
|
+
}
|
|
11060
|
+
if (visible)
|
|
11061
|
+
current.push(this.project(p.mx, p.my, p.v));
|
|
11062
|
+
}
|
|
11063
|
+
if (current.length > 0)
|
|
11064
|
+
parts.push(current);
|
|
11065
|
+
return parts.filter((part) => part.length > 1);
|
|
11066
|
+
}
|
|
11067
|
+
/**
|
|
11068
|
+
* Clips the rings of a polygon to the visible hemisphere.
|
|
11069
|
+
*
|
|
11070
|
+
* Every ring crossing the horizon is cut into its visible chains, each running from an
|
|
11071
|
+
* entry point to an exit point on the horizon. The chains are then reconnected along the
|
|
11072
|
+
* silhouette: from an exit point, the silhouette is followed to the nearest entry point
|
|
11073
|
+
* (of any chain of any ring). Rings follow the vector tile winding (exterior rings
|
|
11074
|
+
* clockwise on screen, holes counter-clockwise), which keeps the filled area on the right
|
|
11075
|
+
* of every ring, so the silhouette is always walked clockwise.
|
|
11076
|
+
*/
|
|
11077
|
+
#clipPolygon(rings) {
|
|
11078
|
+
const project = (p) => this.project(p.mx, p.my, p.v);
|
|
11079
|
+
if (!this.clipsHorizon)
|
|
11080
|
+
return rings.map((ring) => ring.map(project));
|
|
11081
|
+
const result = [];
|
|
11082
|
+
const chains = [];
|
|
11083
|
+
const angleOf = (p) => Math.atan2(p.v[1], p.v[0]);
|
|
11084
|
+
for (const ring of rings) {
|
|
11085
|
+
const count = ring.length;
|
|
11086
|
+
const visible = ring.map((p) => this.isVisible(p.v));
|
|
11087
|
+
// Start walking at an entry point, so every chain is complete.
|
|
11088
|
+
let start = -1;
|
|
11089
|
+
for (let i = 0; i < count; i++) {
|
|
11090
|
+
if (visible[i] && !visible[(i + count - 1) % count]) {
|
|
11091
|
+
start = i;
|
|
11092
|
+
break;
|
|
11093
|
+
}
|
|
11094
|
+
}
|
|
11095
|
+
if (start < 0) {
|
|
11096
|
+
// No crossing: the ring is either completely visible or completely hidden.
|
|
11097
|
+
if (visible[0])
|
|
11098
|
+
result.push(ring.map(project));
|
|
11099
|
+
continue;
|
|
11100
|
+
}
|
|
11101
|
+
let chain;
|
|
11102
|
+
for (let k = 0; k < count; k++) {
|
|
11103
|
+
const i = (start + k) % count;
|
|
11104
|
+
const p = ring[i];
|
|
11105
|
+
const prev = ring[(i + count - 1) % count];
|
|
11106
|
+
if (visible[i]) {
|
|
11107
|
+
if (!chain) {
|
|
11108
|
+
const c = this.#horizonCrossing(prev, p);
|
|
11109
|
+
chain = { entry: angleOf(c), exit: 0, points: [project(c)] };
|
|
11110
|
+
}
|
|
11111
|
+
chain.points.push(project(p));
|
|
11112
|
+
}
|
|
11113
|
+
else if (chain) {
|
|
11114
|
+
const c = this.#horizonCrossing(prev, p);
|
|
11115
|
+
chain.points.push(project(c));
|
|
11116
|
+
chain.exit = angleOf(c);
|
|
11117
|
+
chains.push(chain);
|
|
11118
|
+
chain = undefined;
|
|
11119
|
+
}
|
|
11120
|
+
}
|
|
11121
|
+
}
|
|
11122
|
+
// Reconnect the chains along the silhouette.
|
|
11123
|
+
const used = new Set();
|
|
11124
|
+
for (const first of chains) {
|
|
11125
|
+
if (used.has(first))
|
|
11126
|
+
continue;
|
|
11127
|
+
const ring = [];
|
|
11128
|
+
let chain = first;
|
|
11129
|
+
for (;;) {
|
|
11130
|
+
used.add(chain);
|
|
11131
|
+
ring.push(...chain.points);
|
|
11132
|
+
let next = first;
|
|
11133
|
+
let bestDelta = clockwiseDelta(chain.exit, first.entry);
|
|
11134
|
+
for (const candidate of chains) {
|
|
11135
|
+
if (used.has(candidate))
|
|
11136
|
+
continue;
|
|
11137
|
+
const delta = clockwiseDelta(chain.exit, candidate.entry);
|
|
11138
|
+
if (delta < bestDelta) {
|
|
11139
|
+
bestDelta = delta;
|
|
11140
|
+
next = candidate;
|
|
11141
|
+
}
|
|
11142
|
+
}
|
|
11143
|
+
this.#pushArc(ring, chain.exit, next.entry);
|
|
11144
|
+
if (next === first)
|
|
11145
|
+
break;
|
|
11146
|
+
chain = next;
|
|
11147
|
+
}
|
|
11148
|
+
result.push(ring);
|
|
11149
|
+
}
|
|
11150
|
+
return result;
|
|
11151
|
+
}
|
|
11152
|
+
/**
|
|
11153
|
+
* Appends points along the horizon circle, clockwise on screen (decreasing angle in the
|
|
11154
|
+
* y-up view frame) from angle a0 to a1.
|
|
11155
|
+
*/
|
|
11156
|
+
#pushArc(result, a0, a1) {
|
|
11157
|
+
const delta = clockwiseDelta(a0, a1);
|
|
11158
|
+
const steps = Math.ceil(delta / (Math.PI / 90));
|
|
11159
|
+
const h = this.#horizonZ;
|
|
11160
|
+
const rho = Math.sqrt(1 - h * h);
|
|
11161
|
+
for (let s = 1; s < steps; s++) {
|
|
11162
|
+
const a = a0 - (delta * s) / steps;
|
|
11163
|
+
const v = [rho * Math.cos(a), rho * Math.sin(a), h];
|
|
11164
|
+
const [mx, my] = this.fromSphere(v);
|
|
11165
|
+
result.push(this.project(mx, my, v));
|
|
11166
|
+
}
|
|
11167
|
+
}
|
|
11168
|
+
/**
|
|
11169
|
+
* Splits a raster tile into a mesh of triangles, each small enough to be drawn with an
|
|
11170
|
+
* affine transform (see {@link RasterTriangle}). An affine transform is fully determined
|
|
11171
|
+
* by three points, so every triangle meets its projected corners exactly and neighbouring
|
|
11172
|
+
* triangles share their edges. Cells on the far side of the globe are dropped.
|
|
11173
|
+
*/
|
|
11174
|
+
rasterTriangles(tile) {
|
|
11175
|
+
const tileSize = 1 / 2 ** tile.z;
|
|
11176
|
+
const maxLength = this.maxSegmentLength * Math.sqrt(MAX_RASTER_ERROR_PX / MAX_CURVE_ERROR_PX);
|
|
11177
|
+
const n = Math.max(1, Math.min(MAX_RASTER_CELLS, Math.ceil(tileSize / maxLength)));
|
|
11178
|
+
// Project the (n+1)² grid corners once.
|
|
11179
|
+
const corners = [];
|
|
11180
|
+
for (let j = 0; j <= n; j++) {
|
|
11181
|
+
for (let i = 0; i <= n; i++) {
|
|
11182
|
+
const mx = (tile.x + i / n) * tileSize;
|
|
11183
|
+
const my = (tile.y + j / n) * tileSize;
|
|
11184
|
+
let v = this.toSphere(mx, my);
|
|
11185
|
+
if (this.isVisible(v)) {
|
|
11186
|
+
corners.push(this.project(mx, my, v));
|
|
11187
|
+
}
|
|
11188
|
+
else {
|
|
11189
|
+
// Pull the corner onto the horizon, so cells at the silhouette end there.
|
|
11190
|
+
if (this.#isBehind(v)) {
|
|
11191
|
+
corners.push(undefined);
|
|
11192
|
+
continue;
|
|
11193
|
+
}
|
|
11194
|
+
v = this.#clampToHorizon(v);
|
|
11195
|
+
const [cx, cy] = this.fromSphere(v);
|
|
11196
|
+
corners.push(this.project(cx, cy, v));
|
|
11197
|
+
}
|
|
11198
|
+
}
|
|
11199
|
+
}
|
|
11200
|
+
const triangles = [];
|
|
11201
|
+
for (let j = 0; j < n; j++) {
|
|
11202
|
+
for (let i = 0; i < n; i++) {
|
|
11203
|
+
const p00 = corners[j * (n + 1) + i];
|
|
11204
|
+
const p10 = corners[j * (n + 1) + i + 1];
|
|
11205
|
+
const p01 = corners[(j + 1) * (n + 1) + i];
|
|
11206
|
+
const p11 = corners[(j + 1) * (n + 1) + i + 1];
|
|
11207
|
+
if (!p00 || !p10 || !p01 || !p11)
|
|
11208
|
+
continue;
|
|
11209
|
+
const minX = Math.min(p00.x, p10.x, p01.x, p11.x);
|
|
11210
|
+
const maxX = Math.max(p00.x, p10.x, p01.x, p11.x);
|
|
11211
|
+
const minY = Math.min(p00.y, p10.y, p01.y, p11.y);
|
|
11212
|
+
const maxY = Math.max(p00.y, p10.y, p01.y, p11.y);
|
|
11213
|
+
if (maxX < 0 || minX > this.width || maxY < 0 || minY > this.height)
|
|
11214
|
+
continue;
|
|
11215
|
+
if (!this.#isCellVisible(tile, n, i, j))
|
|
11216
|
+
continue;
|
|
11217
|
+
const u0 = i / n;
|
|
11218
|
+
const v0 = j / n;
|
|
11219
|
+
const u1 = (i + 1) / n;
|
|
11220
|
+
const v1 = (j + 1) / n;
|
|
11221
|
+
triangles.push({
|
|
11222
|
+
source: [
|
|
11223
|
+
[u0, v0],
|
|
11224
|
+
[u1, v0],
|
|
11225
|
+
[u1, v1],
|
|
11226
|
+
],
|
|
11227
|
+
target: [
|
|
11228
|
+
[p00.x, p00.y],
|
|
11229
|
+
[p10.x, p10.y],
|
|
11230
|
+
[p11.x, p11.y],
|
|
11231
|
+
],
|
|
11232
|
+
}, {
|
|
11233
|
+
source: [
|
|
11234
|
+
[u0, v0],
|
|
11235
|
+
[u1, v1],
|
|
11236
|
+
[u0, v1],
|
|
11237
|
+
],
|
|
11238
|
+
target: [
|
|
11239
|
+
[p00.x, p00.y],
|
|
11240
|
+
[p11.x, p11.y],
|
|
11241
|
+
[p01.x, p01.y],
|
|
11242
|
+
],
|
|
11243
|
+
});
|
|
11244
|
+
}
|
|
11245
|
+
}
|
|
11246
|
+
return triangles;
|
|
11247
|
+
}
|
|
11248
|
+
/** Whether any part of a raster cell is on the visible side of the globe. */
|
|
11249
|
+
#isCellVisible(tile, n, i, j) {
|
|
11250
|
+
if (!this.clipsHorizon)
|
|
11251
|
+
return true;
|
|
11252
|
+
const tileSize = 1 / 2 ** tile.z;
|
|
11253
|
+
for (let dj = 0; dj <= 2; dj++) {
|
|
11254
|
+
for (let di = 0; di <= 2; di++) {
|
|
11255
|
+
const mx = (tile.x + (i + di / 2) / n) * tileSize;
|
|
11256
|
+
const my = (tile.y + (j + dj / 2) / n) * tileSize;
|
|
11257
|
+
if (this.isVisible(this.toSphere(mx, my)))
|
|
11258
|
+
return true;
|
|
11259
|
+
}
|
|
11260
|
+
}
|
|
11261
|
+
return false;
|
|
11262
|
+
}
|
|
11263
|
+
/** Moves a point on the far side of the globe onto the horizon (keeping its direction on screen). */
|
|
11264
|
+
#clampToHorizon(v) {
|
|
11265
|
+
const h = this.#horizonZ;
|
|
11266
|
+
const rho = Math.sqrt(1 - h * h);
|
|
11267
|
+
const len = Math.hypot(v[0], v[1]);
|
|
11268
|
+
if (len < 1e-12)
|
|
11269
|
+
return [rho, 0, h];
|
|
11270
|
+
return [(v[0] / len) * rho, (v[1] / len) * rho, h];
|
|
11271
|
+
}
|
|
11272
|
+
/** Points almost opposite the view direction have no meaningful place on the horizon. */
|
|
11273
|
+
#isBehind(v) {
|
|
11274
|
+
return Math.hypot(v[0], v[1]) < 1e-9;
|
|
11275
|
+
}
|
|
11276
|
+
/**
|
|
11277
|
+
* Tiles at zoom level `z` that are visible on screen, found by walking down the tile
|
|
11278
|
+
* pyramid and projecting a grid of sample points of each tile.
|
|
11279
|
+
*/
|
|
11280
|
+
coveringTiles(z) {
|
|
11281
|
+
const result = [];
|
|
11282
|
+
const visit = (tz, tx, ty) => {
|
|
11283
|
+
if (tz >= 2 && !this.#isTileVisible(tz, tx, ty))
|
|
11284
|
+
return;
|
|
11285
|
+
if (tz === z) {
|
|
11286
|
+
result.push({ x: tx, y: ty, z: tz });
|
|
11287
|
+
return;
|
|
11288
|
+
}
|
|
11289
|
+
for (let dy = 0; dy < 2; dy++) {
|
|
11290
|
+
for (let dx = 0; dx < 2; dx++)
|
|
11291
|
+
visit(tz + 1, tx * 2 + dx, ty * 2 + dy);
|
|
11292
|
+
}
|
|
11293
|
+
};
|
|
11294
|
+
visit(0, 0, 0);
|
|
11295
|
+
return result;
|
|
11296
|
+
}
|
|
11297
|
+
/**
|
|
11298
|
+
* Whether a tile, including a margin around it (the tile buffer, as a fraction of the tile
|
|
11299
|
+
* size), lies completely on the visible side of the globe, so no horizon clipping happens.
|
|
11300
|
+
*/
|
|
11301
|
+
isTileFullyVisible(tile, margin = 0.1) {
|
|
11302
|
+
if (!this.clipsHorizon)
|
|
11303
|
+
return true;
|
|
11304
|
+
const size = 1 / 2 ** tile.z;
|
|
11305
|
+
const samples = 8;
|
|
11306
|
+
for (let j = 0; j <= samples; j++) {
|
|
11307
|
+
for (let i = 0; i <= samples; i++) {
|
|
11308
|
+
if (i !== 0 && i !== samples && j !== 0 && j !== samples)
|
|
11309
|
+
continue; // the border only
|
|
11310
|
+
const mx = (tile.x - margin + ((1 + 2 * margin) * i) / samples) * size;
|
|
11311
|
+
const my = (tile.y - margin + ((1 + 2 * margin) * j) / samples) * size;
|
|
11312
|
+
if (!this.isVisible(this.toSphere(mx, my)))
|
|
11313
|
+
return false;
|
|
11314
|
+
}
|
|
11315
|
+
}
|
|
11316
|
+
return true;
|
|
11317
|
+
}
|
|
11318
|
+
#isTileVisible(z, x, y) {
|
|
11319
|
+
const samples = 6;
|
|
11320
|
+
const size = 1 / 2 ** z;
|
|
11321
|
+
let minX = Infinity;
|
|
11322
|
+
let minY = Infinity;
|
|
11323
|
+
let maxX = -Infinity;
|
|
11324
|
+
let maxY = -Infinity;
|
|
11325
|
+
const addSample = (mx, my) => {
|
|
11326
|
+
const v = this.toSphere(mx, my);
|
|
11327
|
+
if (!this.isVisible(v))
|
|
11328
|
+
return;
|
|
11329
|
+
const p = this.project(mx, my, v);
|
|
11330
|
+
if (p.x < minX)
|
|
11331
|
+
minX = p.x;
|
|
11332
|
+
if (p.y < minY)
|
|
11333
|
+
minY = p.y;
|
|
11334
|
+
if (p.x > maxX)
|
|
11335
|
+
maxX = p.x;
|
|
11336
|
+
if (p.y > maxY)
|
|
11337
|
+
maxY = p.y;
|
|
11338
|
+
};
|
|
11339
|
+
for (let j = 0; j <= samples; j++) {
|
|
11340
|
+
for (let i = 0; i <= samples; i++)
|
|
11341
|
+
addSample((x + i / samples) * size, (y + j / samples) * size);
|
|
11342
|
+
}
|
|
11343
|
+
// Zoomed in, the visible part of the globe can be much smaller than the sample spacing.
|
|
11344
|
+
// So also sample the point of the tile closest to the map center.
|
|
11345
|
+
const x0 = x * size;
|
|
11346
|
+
const centerX = this.#centerX - Math.round(this.#centerX - (x0 + size / 2));
|
|
11347
|
+
addSample(clamp(centerX, x0, x0 + size), clamp(this.#centerY, y * size, (y + 1) * size));
|
|
11348
|
+
// A generous margin: the samples may miss the part of the tile at the silhouette.
|
|
11349
|
+
const margin = Math.max(maxX - minX, maxY - minY) / samples + 1;
|
|
11350
|
+
return (minX - margin <= this.width &&
|
|
11351
|
+
maxX + margin >= 0 &&
|
|
11352
|
+
minY - margin <= this.height &&
|
|
11353
|
+
maxY + margin >= 0);
|
|
11354
|
+
}
|
|
11355
|
+
}
|
|
11356
|
+
function clamp(value, min, max) {
|
|
11357
|
+
return Math.min(max, Math.max(min, value));
|
|
11358
|
+
}
|
|
11359
|
+
|
|
10315
11360
|
function resolveTokens(text, properties) {
|
|
10316
11361
|
return text.replace(/\{([^}]+)\}/g, (_, key) => {
|
|
10317
11362
|
const value = properties[key];
|
|
@@ -10324,9 +11369,27 @@ function resolveTokens(text, properties) {
|
|
|
10324
11369
|
return '';
|
|
10325
11370
|
});
|
|
10326
11371
|
}
|
|
10327
|
-
|
|
11372
|
+
/**
|
|
11373
|
+
* Draws the map described by `job` onto its renderer and returns that renderer, so a
|
|
11374
|
+
* caller can take the result in whatever form the backend provides (see
|
|
11375
|
+
* {@link renderMap} for the SVG string, or the PNG entry point for an image buffer).
|
|
11376
|
+
*/
|
|
11377
|
+
async function drawMap(job) {
|
|
11378
|
+
job.projection ??= Projection.fromStyle({
|
|
11379
|
+
width: job.renderer.width,
|
|
11380
|
+
height: job.renderer.height,
|
|
11381
|
+
center: job.view.center,
|
|
11382
|
+
zoom: job.view.zoom,
|
|
11383
|
+
projection: job.style.projection,
|
|
11384
|
+
});
|
|
11385
|
+
const clipCircle = job.projection.clipCircle;
|
|
11386
|
+
if (clipCircle)
|
|
11387
|
+
job.renderer.setClipCircle?.(clipCircle);
|
|
10328
11388
|
await render(job);
|
|
10329
|
-
return job.renderer
|
|
11389
|
+
return job.renderer;
|
|
11390
|
+
}
|
|
11391
|
+
async function renderMap(job) {
|
|
11392
|
+
return (await drawMap(job)).getString();
|
|
10330
11393
|
}
|
|
10331
11394
|
function getFeatures(layerFeatures, layerStyle) {
|
|
10332
11395
|
return layerFeatures.get(layerStyle.sourceLayer) ?? layerFeatures.get(layerStyle.source);
|
|
@@ -10439,7 +11502,7 @@ async function render(job) {
|
|
|
10439
11502
|
case 'raster':
|
|
10440
11503
|
{
|
|
10441
11504
|
const tiles = await getRasterTiles(job, layerStyle.source);
|
|
10442
|
-
renderer.drawRasterTiles(layerId, tiles, {
|
|
11505
|
+
await renderer.drawRasterTiles(layerId, tiles, {
|
|
10443
11506
|
opacity: getPaint('raster-opacity'),
|
|
10444
11507
|
hueRotate: getPaint('raster-hue-rotate'),
|
|
10445
11508
|
brightnessMin: getPaint('raster-brightness-min'),
|
|
@@ -10491,7 +11554,7 @@ async function render(job) {
|
|
|
10491
11554
|
if (symbolFeatures.length === 0)
|
|
10492
11555
|
continue;
|
|
10493
11556
|
// Render icons first (underneath text)
|
|
10494
|
-
renderer.drawIcons(`${layerId}-icons`, symbolFeatures.flatMap((feature) => {
|
|
11557
|
+
await renderer.drawIcons(`${layerId}-icons`, symbolFeatures.flatMap((feature) => {
|
|
10495
11558
|
const iconImage = getLayout('icon-image', feature);
|
|
10496
11559
|
const iconName = iconImage != null
|
|
10497
11560
|
? resolveTokens(iconImage.toString(), feature.properties)
|
|
@@ -10555,6 +11618,61 @@ async function render(job) {
|
|
|
10555
11618
|
}
|
|
10556
11619
|
}
|
|
10557
11620
|
|
|
11621
|
+
/**
|
|
11622
|
+
* Renders a MapLibre style to an SVG image.
|
|
11623
|
+
*
|
|
11624
|
+
* Fetches the vector and raster tiles the view needs, then draws every layer as SVG
|
|
11625
|
+
* elements: fills and lines as paths, raster tiles and sprites as embedded images. The
|
|
11626
|
+
* result is a single self-contained SVG document — it references no external files, so
|
|
11627
|
+
* it can be saved, inlined into HTML or opened in a vector editor as is.
|
|
11628
|
+
*
|
|
11629
|
+
* Works in Node.js (22 or later) and in the browser. The same function is exported by
|
|
11630
|
+
* `@versatiles/svg-renderer`, `@versatiles/png-renderer` and
|
|
11631
|
+
* `@versatiles/maplibre-svg-export`; the examples import it from the first.
|
|
11632
|
+
*
|
|
11633
|
+
* @example Render a map in Node.js
|
|
11634
|
+
* ```ts
|
|
11635
|
+
* import { renderToSVG } from '@versatiles/svg-renderer';
|
|
11636
|
+
* import { inlineSources, osm } from '@versatiles/style';
|
|
11637
|
+
* import { writeFile } from 'node:fs/promises';
|
|
11638
|
+
*
|
|
11639
|
+
* // inlineSources() resolves the style's TileJSON sources into tile URLs,
|
|
11640
|
+
* // which the renderer needs. Without it the map comes out empty.
|
|
11641
|
+
* const style = await inlineSources(osm({ theme: 'colorful' }));
|
|
11642
|
+
*
|
|
11643
|
+
* const svg = await renderToSVG({
|
|
11644
|
+
* style,
|
|
11645
|
+
* width: 800,
|
|
11646
|
+
* height: 600,
|
|
11647
|
+
* lon: 13.4, // Berlin
|
|
11648
|
+
* lat: 52.52,
|
|
11649
|
+
* zoom: 12,
|
|
11650
|
+
* });
|
|
11651
|
+
*
|
|
11652
|
+
* await writeFile('berlin.svg', svg);
|
|
11653
|
+
* ```
|
|
11654
|
+
*
|
|
11655
|
+
* @example Render a hosted style in the browser
|
|
11656
|
+
* ```ts
|
|
11657
|
+
* import { renderToSVG } from '@versatiles/svg-renderer';
|
|
11658
|
+
*
|
|
11659
|
+
* // This hosted style already lists its tile URLs, so it can be used directly.
|
|
11660
|
+
* const style = await fetch(
|
|
11661
|
+
* 'https://tiles.versatiles.org/assets/styles/colorful/style.json',
|
|
11662
|
+
* ).then((response) => response.json());
|
|
11663
|
+
*
|
|
11664
|
+
* document.body.innerHTML = await renderToSVG({ style, width: 800, height: 600, zoom: 3 });
|
|
11665
|
+
* ```
|
|
11666
|
+
*
|
|
11667
|
+
* @example Include labels and icons
|
|
11668
|
+
* ```ts
|
|
11669
|
+
* const svg = await renderToSVG({ style, lon: 13.4, lat: 52.52, zoom: 14, renderLabels: true });
|
|
11670
|
+
* ```
|
|
11671
|
+
*
|
|
11672
|
+
* @param options - What to render and how large. Only `style` is required.
|
|
11673
|
+
* @returns The SVG document, as a string beginning with `<svg`.
|
|
11674
|
+
* @throws If `width` or `height` is not positive.
|
|
11675
|
+
*/
|
|
10558
11676
|
async function renderToSVG(options) {
|
|
10559
11677
|
const width = options.width ?? 1024;
|
|
10560
11678
|
const height = options.height ?? 1024;
|