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