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