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