@versatiles/svg-renderer 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -63
- package/dist/index.cjs +1988 -927
- package/dist/index.js +1988 -927
- package/dist/maplibre-svg-export.js +1988 -927
- package/dist/maplibre-svg-export.umd.js +1988 -927
- package/package.json +22 -21
|
@@ -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",
|
|
@@ -634,19 +631,28 @@
|
|
|
634
631
|
},
|
|
635
632
|
"property-type": "data-constant"
|
|
636
633
|
} },
|
|
637
|
-
"layout_fill-extrusion": {
|
|
638
|
-
"
|
|
639
|
-
|
|
640
|
-
"
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
"
|
|
646
|
-
|
|
634
|
+
"layout_fill-extrusion": {
|
|
635
|
+
"visibility": {
|
|
636
|
+
"type": "enum",
|
|
637
|
+
"values": {
|
|
638
|
+
"visible": {},
|
|
639
|
+
"none": {}
|
|
640
|
+
},
|
|
641
|
+
"default": "visible",
|
|
642
|
+
"expression": {
|
|
643
|
+
"interpolated": false,
|
|
644
|
+
"parameters": ["global-state"]
|
|
645
|
+
},
|
|
646
|
+
"property-type": "data-constant"
|
|
647
647
|
},
|
|
648
|
-
"
|
|
649
|
-
|
|
648
|
+
"fill-extrusion-rounded-corner-distance": {
|
|
649
|
+
"type": "number",
|
|
650
|
+
"default": 0,
|
|
651
|
+
"minimum": 0,
|
|
652
|
+
"units": "meters",
|
|
653
|
+
"property-type": "constant"
|
|
654
|
+
}
|
|
655
|
+
},
|
|
650
656
|
layout_line: {
|
|
651
657
|
"line-cap": {
|
|
652
658
|
"type": "enum",
|
|
@@ -831,9 +837,9 @@
|
|
|
831
837
|
"requires": ["icon-image"],
|
|
832
838
|
"expression": {
|
|
833
839
|
"interpolated": false,
|
|
834
|
-
"parameters": ["zoom"]
|
|
840
|
+
"parameters": ["zoom", "feature"]
|
|
835
841
|
},
|
|
836
|
-
"property-type": "data-
|
|
842
|
+
"property-type": "data-driven"
|
|
837
843
|
},
|
|
838
844
|
"icon-size": {
|
|
839
845
|
"type": "number",
|
|
@@ -1294,6 +1300,31 @@
|
|
|
1294
1300
|
},
|
|
1295
1301
|
"property-type": "data-constant"
|
|
1296
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
|
+
},
|
|
1297
1328
|
"visibility": {
|
|
1298
1329
|
"type": "enum",
|
|
1299
1330
|
"values": {
|
|
@@ -1769,7 +1800,6 @@
|
|
|
1769
1800
|
"fill-extrusion-height": {
|
|
1770
1801
|
"type": "number",
|
|
1771
1802
|
"default": 0,
|
|
1772
|
-
"minimum": 0,
|
|
1773
1803
|
"units": "meters",
|
|
1774
1804
|
"transition": true,
|
|
1775
1805
|
"expression": {
|
|
@@ -1785,7 +1815,6 @@
|
|
|
1785
1815
|
"fill-extrusion-base": {
|
|
1786
1816
|
"type": "number",
|
|
1787
1817
|
"default": 0,
|
|
1788
|
-
"minimum": 0,
|
|
1789
1818
|
"units": "meters",
|
|
1790
1819
|
"transition": true,
|
|
1791
1820
|
"requires": ["fill-extrusion-height"],
|
|
@@ -2820,44 +2849,10 @@
|
|
|
2820
2849
|
}
|
|
2821
2850
|
}
|
|
2822
2851
|
};
|
|
2823
|
-
|
|
2852
|
+
|
|
2824
2853
|
//#region src/reference/latest.ts
|
|
2825
2854
|
const latest = v8_default;
|
|
2826
|
-
|
|
2827
|
-
//#region src/expression/parsing_error.ts
|
|
2828
|
-
var ExpressionParsingError = class extends Error {
|
|
2829
|
-
constructor(key, message) {
|
|
2830
|
-
super(message);
|
|
2831
|
-
this.message = message;
|
|
2832
|
-
this.key = key;
|
|
2833
|
-
}
|
|
2834
|
-
};
|
|
2835
|
-
//#endregion
|
|
2836
|
-
//#region src/expression/scope.ts
|
|
2837
|
-
/**
|
|
2838
|
-
* Tracks `let` bindings during expression parsing.
|
|
2839
|
-
* @private
|
|
2840
|
-
*/
|
|
2841
|
-
var Scope = class Scope {
|
|
2842
|
-
constructor(parent, bindings = []) {
|
|
2843
|
-
this.parent = parent;
|
|
2844
|
-
this.bindings = {};
|
|
2845
|
-
for (const [name, expression] of bindings) this.bindings[name] = expression;
|
|
2846
|
-
}
|
|
2847
|
-
concat(bindings) {
|
|
2848
|
-
return new Scope(this, bindings);
|
|
2849
|
-
}
|
|
2850
|
-
get(name) {
|
|
2851
|
-
if (this.bindings[name]) return this.bindings[name];
|
|
2852
|
-
if (this.parent) return this.parent.get(name);
|
|
2853
|
-
throw new Error(`${name} not found in scope.`);
|
|
2854
|
-
}
|
|
2855
|
-
has(name) {
|
|
2856
|
-
if (this.bindings[name]) return true;
|
|
2857
|
-
return this.parent ? this.parent.has(name) : false;
|
|
2858
|
-
}
|
|
2859
|
-
};
|
|
2860
|
-
//#endregion
|
|
2855
|
+
|
|
2861
2856
|
//#region src/expression/types.ts
|
|
2862
2857
|
const NullType = { kind: "null" };
|
|
2863
2858
|
const NumberType = { kind: "number" };
|
|
@@ -2953,7 +2948,7 @@
|
|
|
2953
2948
|
if (provided.kind === "array" && sample.kind === "array") return provided.itemType.kind === sample.itemType.kind && typeof provided.N === "number";
|
|
2954
2949
|
return provided.kind === sample.kind;
|
|
2955
2950
|
}
|
|
2956
|
-
|
|
2951
|
+
|
|
2957
2952
|
//#region src/expression/types/color_spaces.ts
|
|
2958
2953
|
const Xn = .96422;
|
|
2959
2954
|
const Yn = 1;
|
|
@@ -3048,7 +3043,7 @@
|
|
|
3048
3043
|
alpha
|
|
3049
3044
|
];
|
|
3050
3045
|
}
|
|
3051
|
-
|
|
3046
|
+
|
|
3052
3047
|
//#region src/util/get_own.ts
|
|
3053
3048
|
const hasOwnProperty = Object.hasOwn || function hasOwnProperty(object, key) {
|
|
3054
3049
|
return Object.prototype.hasOwnProperty.call(object, key);
|
|
@@ -3056,7 +3051,7 @@
|
|
|
3056
3051
|
function getOwn(object, key) {
|
|
3057
3052
|
return hasOwnProperty(object, key) ? object[key] : void 0;
|
|
3058
3053
|
}
|
|
3059
|
-
|
|
3054
|
+
|
|
3060
3055
|
//#region src/expression/types/parse_css_color.ts
|
|
3061
3056
|
/**
|
|
3062
3057
|
* CSS color parser compliant with CSS Color 4 Specification.
|
|
@@ -3135,9 +3130,9 @@
|
|
|
3135
3130
|
const maxValue = valFormat === "%%%" ? 100 : valFormat === "" ? 255 : 0;
|
|
3136
3131
|
if (maxValue) {
|
|
3137
3132
|
const rgba = [
|
|
3138
|
-
clamp(+r / maxValue, 0, 1),
|
|
3139
|
-
clamp(+g / maxValue, 0, 1),
|
|
3140
|
-
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),
|
|
3141
3136
|
a ? parseAlpha(+a, ap) : 1
|
|
3142
3137
|
];
|
|
3143
3138
|
if (validateNumbers(rgba)) return rgba;
|
|
@@ -3157,8 +3152,8 @@
|
|
|
3157
3152
|
if (argFormat === " " || argFormat === " /" || argFormat === ",," || argFormat === ",,,") {
|
|
3158
3153
|
const hsla = [
|
|
3159
3154
|
+h,
|
|
3160
|
-
clamp(+s, 0, 100),
|
|
3161
|
-
clamp(+l, 0, 100),
|
|
3155
|
+
clamp$1(+s, 0, 100),
|
|
3156
|
+
clamp$1(+l, 0, 100),
|
|
3162
3157
|
a ? parseAlpha(+a, ap) : 1
|
|
3163
3158
|
];
|
|
3164
3159
|
if (validateNumbers(hsla)) return hslToRgb(hsla);
|
|
@@ -3169,9 +3164,9 @@
|
|
|
3169
3164
|
return parseInt(hex.padEnd(2, hex), 16) / 255;
|
|
3170
3165
|
}
|
|
3171
3166
|
function parseAlpha(a, asPercentage) {
|
|
3172
|
-
return clamp(asPercentage ? a / 100 : a, 0, 1);
|
|
3167
|
+
return clamp$1(asPercentage ? a / 100 : a, 0, 1);
|
|
3173
3168
|
}
|
|
3174
|
-
function clamp(n, min, max) {
|
|
3169
|
+
function clamp$1(n, min, max) {
|
|
3175
3170
|
return Math.min(Math.max(min, n), max);
|
|
3176
3171
|
}
|
|
3177
3172
|
/**
|
|
@@ -3934,7 +3929,7 @@
|
|
|
3934
3929
|
50
|
|
3935
3930
|
]
|
|
3936
3931
|
};
|
|
3937
|
-
|
|
3932
|
+
|
|
3938
3933
|
//#region src/util/interpolate-primitives.ts
|
|
3939
3934
|
function interpolateNumber(from, to, t) {
|
|
3940
3935
|
return from + t * (to - from);
|
|
@@ -3944,7 +3939,7 @@
|
|
|
3944
3939
|
return interpolateNumber(d, to[i], t);
|
|
3945
3940
|
});
|
|
3946
3941
|
}
|
|
3947
|
-
|
|
3942
|
+
|
|
3948
3943
|
//#region src/expression/types/color.ts
|
|
3949
3944
|
/**
|
|
3950
3945
|
* Checks whether the specified color space is one of the supported interpolation color spaces.
|
|
@@ -4128,26 +4123,7 @@
|
|
|
4128
4123
|
}
|
|
4129
4124
|
}
|
|
4130
4125
|
};
|
|
4131
|
-
|
|
4132
|
-
//#region src/expression/types/collator.ts
|
|
4133
|
-
var Collator = class {
|
|
4134
|
-
constructor(caseSensitive, diacriticSensitive, locale) {
|
|
4135
|
-
if (caseSensitive) this.sensitivity = diacriticSensitive ? "variant" : "case";
|
|
4136
|
-
else this.sensitivity = diacriticSensitive ? "accent" : "base";
|
|
4137
|
-
this.locale = locale;
|
|
4138
|
-
this.collator = new Intl.Collator(this.locale ? this.locale : [], {
|
|
4139
|
-
sensitivity: this.sensitivity,
|
|
4140
|
-
usage: "search"
|
|
4141
|
-
});
|
|
4142
|
-
}
|
|
4143
|
-
compare(lhs, rhs) {
|
|
4144
|
-
return this.collator.compare(lhs, rhs);
|
|
4145
|
-
}
|
|
4146
|
-
resolvedLocale() {
|
|
4147
|
-
return new Intl.Collator(this.locale ? this.locale : []).resolvedOptions().locale;
|
|
4148
|
-
}
|
|
4149
|
-
};
|
|
4150
|
-
//#endregion
|
|
4126
|
+
|
|
4151
4127
|
//#region src/expression/types/formatted.ts
|
|
4152
4128
|
const VERTICAL_ALIGN_OPTIONS = [
|
|
4153
4129
|
"bottom",
|
|
@@ -4184,7 +4160,7 @@
|
|
|
4184
4160
|
return this.sections.map((section) => section.text).join("");
|
|
4185
4161
|
}
|
|
4186
4162
|
};
|
|
4187
|
-
|
|
4163
|
+
|
|
4188
4164
|
//#region src/expression/types/padding.ts
|
|
4189
4165
|
/**
|
|
4190
4166
|
* A set of four numbers representing padding around a box. Create instances from
|
|
@@ -4228,14 +4204,12 @@
|
|
|
4228
4204
|
input[1]
|
|
4229
4205
|
];
|
|
4230
4206
|
break;
|
|
4231
|
-
case 3:
|
|
4232
|
-
input
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
];
|
|
4238
|
-
break;
|
|
4207
|
+
case 3: input = [
|
|
4208
|
+
input[0],
|
|
4209
|
+
input[1],
|
|
4210
|
+
input[2],
|
|
4211
|
+
input[1]
|
|
4212
|
+
];
|
|
4239
4213
|
}
|
|
4240
4214
|
return new Padding(input);
|
|
4241
4215
|
}
|
|
@@ -4246,7 +4220,7 @@
|
|
|
4246
4220
|
return new Padding(interpolateArray(from.values, to.values, t));
|
|
4247
4221
|
}
|
|
4248
4222
|
};
|
|
4249
|
-
|
|
4223
|
+
|
|
4250
4224
|
//#region src/expression/types/number_array.ts
|
|
4251
4225
|
/**
|
|
4252
4226
|
* An array of numbers. Create instances from
|
|
@@ -4276,7 +4250,7 @@
|
|
|
4276
4250
|
return new NumberArray(interpolateArray(from.values, to.values, t));
|
|
4277
4251
|
}
|
|
4278
4252
|
};
|
|
4279
|
-
|
|
4253
|
+
|
|
4280
4254
|
//#region src/expression/types/color_array.ts
|
|
4281
4255
|
/**
|
|
4282
4256
|
* An array of colors. Create instances from
|
|
@@ -4319,7 +4293,7 @@
|
|
|
4319
4293
|
return new ColorArray(colors);
|
|
4320
4294
|
}
|
|
4321
4295
|
};
|
|
4322
|
-
|
|
4296
|
+
|
|
4323
4297
|
//#region src/expression/runtime_error.ts
|
|
4324
4298
|
var RuntimeError = class extends Error {
|
|
4325
4299
|
constructor(message, path) {
|
|
@@ -4331,7 +4305,7 @@
|
|
|
4331
4305
|
return this.message;
|
|
4332
4306
|
}
|
|
4333
4307
|
};
|
|
4334
|
-
|
|
4308
|
+
|
|
4335
4309
|
//#region src/expression/types/variable_anchor_offset_collection.ts
|
|
4336
4310
|
/** Set of valid anchor positions, as a set for validation */
|
|
4337
4311
|
const anchors = /* @__PURE__ */ new Set([
|
|
@@ -4383,7 +4357,7 @@
|
|
|
4383
4357
|
return new VariableAnchorOffsetCollection(output);
|
|
4384
4358
|
}
|
|
4385
4359
|
};
|
|
4386
|
-
|
|
4360
|
+
|
|
4387
4361
|
//#region src/expression/types/resolved_image.ts
|
|
4388
4362
|
var ResolvedImage = class ResolvedImage {
|
|
4389
4363
|
constructor(options) {
|
|
@@ -4401,7 +4375,7 @@
|
|
|
4401
4375
|
});
|
|
4402
4376
|
}
|
|
4403
4377
|
};
|
|
4404
|
-
|
|
4378
|
+
|
|
4405
4379
|
//#region src/expression/types/projection_definition.ts
|
|
4406
4380
|
var ProjectionDefinition = class ProjectionDefinition {
|
|
4407
4381
|
constructor(from, to, transition) {
|
|
@@ -4427,7 +4401,26 @@
|
|
|
4427
4401
|
if (typeof input === "string") return new ProjectionDefinition(input, input, 1);
|
|
4428
4402
|
}
|
|
4429
4403
|
};
|
|
4430
|
-
|
|
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
|
+
|
|
4431
4424
|
//#region src/expression/values.ts
|
|
4432
4425
|
function validateRGBA(r, g, b, a) {
|
|
4433
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" ? [
|
|
@@ -4494,7 +4487,7 @@
|
|
|
4494
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();
|
|
4495
4488
|
else return JSON.stringify(value);
|
|
4496
4489
|
}
|
|
4497
|
-
|
|
4490
|
+
|
|
4498
4491
|
//#region src/expression/definitions/literal.ts
|
|
4499
4492
|
var Literal = class Literal {
|
|
4500
4493
|
constructor(type, value) {
|
|
@@ -4503,7 +4496,7 @@
|
|
|
4503
4496
|
}
|
|
4504
4497
|
static parse(args, context) {
|
|
4505
4498
|
if (args.length !== 2) return context.error(`'literal' expression requires exactly one argument, but found ${args.length - 1} instead.`);
|
|
4506
|
-
if (!isValue(args[1])) return context.error(
|
|
4499
|
+
if (!isValue(args[1])) return context.error(`invalid value of type "${typeof args[1]}"`);
|
|
4507
4500
|
const value = args[1];
|
|
4508
4501
|
let type = typeOf(value);
|
|
4509
4502
|
const expected = context.expectedType;
|
|
@@ -4518,85 +4511,524 @@
|
|
|
4518
4511
|
return true;
|
|
4519
4512
|
}
|
|
4520
4513
|
};
|
|
4521
|
-
|
|
4522
|
-
//#region src/expression/
|
|
4523
|
-
const
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
var
|
|
4530
|
-
constructor(
|
|
4531
|
-
this.
|
|
4532
|
-
this.
|
|
4533
|
-
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;
|
|
4534
4531
|
}
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
let i = 1;
|
|
4538
|
-
let type;
|
|
4539
|
-
const name = args[0];
|
|
4540
|
-
if (name === "array") {
|
|
4541
|
-
let itemType;
|
|
4542
|
-
if (args.length > 2) {
|
|
4543
|
-
const type = args[1];
|
|
4544
|
-
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);
|
|
4545
|
-
itemType = types$1[type];
|
|
4546
|
-
i++;
|
|
4547
|
-
} else itemType = ValueType;
|
|
4548
|
-
let N;
|
|
4549
|
-
if (args.length > 3) {
|
|
4550
|
-
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);
|
|
4551
|
-
N = args[2];
|
|
4552
|
-
i++;
|
|
4553
|
-
}
|
|
4554
|
-
type = array(itemType, N);
|
|
4555
|
-
} else {
|
|
4556
|
-
if (!types$1[name]) throw new Error(`Types doesn't contain name = ${name}`);
|
|
4557
|
-
type = types$1[name];
|
|
4558
|
-
}
|
|
4559
|
-
const parsed = [];
|
|
4560
|
-
for (; i < args.length; i++) {
|
|
4561
|
-
const input = context.parse(args[i], i, ValueType);
|
|
4562
|
-
if (!input) return null;
|
|
4563
|
-
parsed.push(input);
|
|
4564
|
-
}
|
|
4565
|
-
return new Assertion(type, parsed, context.key);
|
|
4532
|
+
id() {
|
|
4533
|
+
return this.feature && "id" in this.feature ? this.feature.id : null;
|
|
4566
4534
|
}
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
const value = this.args[i].evaluate(ctx);
|
|
4570
|
-
if (!checkSubtype(this.type, typeOf(value))) return value;
|
|
4571
|
-
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);
|
|
4572
|
-
}
|
|
4573
|
-
throw new Error();
|
|
4535
|
+
geometryType() {
|
|
4536
|
+
return this.feature ? typeof this.feature.type === "number" ? geometryTypes[this.feature.type] : this.feature.type : null;
|
|
4574
4537
|
}
|
|
4575
|
-
|
|
4576
|
-
this.
|
|
4538
|
+
geometry() {
|
|
4539
|
+
return this.feature && "geometry" in this.feature ? this.feature.geometry : null;
|
|
4577
4540
|
}
|
|
4578
|
-
|
|
4579
|
-
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;
|
|
4580
4554
|
}
|
|
4581
4555
|
};
|
|
4582
|
-
|
|
4583
|
-
//#region src/expression/
|
|
4584
|
-
const types = {
|
|
4585
|
-
"to-boolean": BooleanType,
|
|
4586
|
-
"to-color": ColorType,
|
|
4587
|
-
"to-number": NumberType,
|
|
4588
|
-
"to-string": StringType
|
|
4589
|
-
};
|
|
4556
|
+
|
|
4557
|
+
//#region src/expression/stops.ts
|
|
4590
4558
|
/**
|
|
4591
|
-
*
|
|
4592
|
-
* "to-color". Since these coercions can fail at runtime, they accept multiple
|
|
4593
|
-
* arguments, only evaluating one at a time until one succeeds.
|
|
4594
|
-
*
|
|
4559
|
+
* Returns the index of the last stop <= input, or 0 if it doesn't exist.
|
|
4595
4560
|
* @private
|
|
4596
4561
|
*/
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
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) {
|
|
4584
|
+
this.type = type;
|
|
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;
|
|
4600
5032
|
this.args = args;
|
|
4601
5033
|
this.key = key;
|
|
4602
5034
|
}
|
|
@@ -4659,203 +5091,47 @@
|
|
|
4659
5091
|
input = arg.evaluate(ctx);
|
|
4660
5092
|
const val = ColorArray.parse(input);
|
|
4661
5093
|
if (val) return val;
|
|
4662
|
-
}
|
|
4663
|
-
throw new RuntimeError(`Could not parse colorArray from value '${typeof input === "string" ? input : JSON.stringify(input)}'`, this.key);
|
|
4664
|
-
}
|
|
4665
|
-
case "variableAnchorOffsetCollection": {
|
|
4666
|
-
let input;
|
|
4667
|
-
for (const arg of this.args) {
|
|
4668
|
-
input = arg.evaluate(ctx);
|
|
4669
|
-
const coll = VariableAnchorOffsetCollection.parse(input);
|
|
4670
|
-
if (coll) return coll;
|
|
4671
|
-
}
|
|
4672
|
-
throw new RuntimeError(`Could not parse variableAnchorOffsetCollection from value '${typeof input === "string" ? input : JSON.stringify(input)}'`, this.key);
|
|
4673
|
-
}
|
|
4674
|
-
case "number": {
|
|
4675
|
-
let value = null;
|
|
4676
|
-
for (const arg of this.args) {
|
|
4677
|
-
value = arg.evaluate(ctx);
|
|
4678
|
-
if (value === null) return 0;
|
|
4679
|
-
const num = Number(value);
|
|
4680
|
-
if (isNaN(num)) continue;
|
|
4681
|
-
return num;
|
|
4682
|
-
}
|
|
4683
|
-
throw new RuntimeError(`Could not convert ${JSON.stringify(value)} to number.`, this.key);
|
|
4684
|
-
}
|
|
4685
|
-
case "formatted": return Formatted.fromString(valueToString(this.args[0].evaluate(ctx)));
|
|
4686
|
-
case "resolvedImage": return ResolvedImage.fromString(valueToString(this.args[0].evaluate(ctx)));
|
|
4687
|
-
case "projectionDefinition": return this.args[0].evaluate(ctx);
|
|
4688
|
-
default: return valueToString(this.args[0].evaluate(ctx));
|
|
4689
|
-
}
|
|
4690
|
-
}
|
|
4691
|
-
eachChild(fn) {
|
|
4692
|
-
this.args.forEach(fn);
|
|
4693
|
-
}
|
|
4694
|
-
outputDefined() {
|
|
4695
|
-
return this.args.every((arg) => arg.outputDefined());
|
|
4696
|
-
}
|
|
4697
|
-
};
|
|
4698
|
-
//#endregion
|
|
4699
|
-
//#region src/expression/evaluation_context.ts
|
|
4700
|
-
const geometryTypes = [
|
|
4701
|
-
"Unknown",
|
|
4702
|
-
"Point",
|
|
4703
|
-
"LineString",
|
|
4704
|
-
"Polygon"
|
|
4705
|
-
];
|
|
4706
|
-
var EvaluationContext = class {
|
|
4707
|
-
constructor() {
|
|
4708
|
-
this.globals = null;
|
|
4709
|
-
this.feature = null;
|
|
4710
|
-
this.featureState = null;
|
|
4711
|
-
this.formattedSection = null;
|
|
4712
|
-
this._parseColorCache = /* @__PURE__ */ new Map();
|
|
4713
|
-
this.availableImages = null;
|
|
4714
|
-
this.canonical = null;
|
|
4715
|
-
}
|
|
4716
|
-
id() {
|
|
4717
|
-
return this.feature && "id" in this.feature ? this.feature.id : null;
|
|
4718
|
-
}
|
|
4719
|
-
geometryType() {
|
|
4720
|
-
return this.feature ? typeof this.feature.type === "number" ? geometryTypes[this.feature.type] : this.feature.type : null;
|
|
4721
|
-
}
|
|
4722
|
-
geometry() {
|
|
4723
|
-
return this.feature && "geometry" in this.feature ? this.feature.geometry : null;
|
|
4724
|
-
}
|
|
4725
|
-
canonicalID() {
|
|
4726
|
-
return this.canonical;
|
|
4727
|
-
}
|
|
4728
|
-
properties() {
|
|
4729
|
-
return this.feature && this.feature.properties || {};
|
|
4730
|
-
}
|
|
4731
|
-
parseColor(input) {
|
|
4732
|
-
let cached = this._parseColorCache.get(input);
|
|
4733
|
-
if (!cached) {
|
|
4734
|
-
cached = Color$1.parse(input);
|
|
4735
|
-
this._parseColorCache.set(input, cached);
|
|
4736
|
-
}
|
|
4737
|
-
return cached;
|
|
4738
|
-
}
|
|
4739
|
-
};
|
|
4740
|
-
//#endregion
|
|
4741
|
-
//#region src/expression/parsing_context.ts
|
|
4742
|
-
/**
|
|
4743
|
-
* State associated parsing at a given point in an expression tree.
|
|
4744
|
-
* @private
|
|
4745
|
-
*/
|
|
4746
|
-
var ParsingContext = class ParsingContext {
|
|
4747
|
-
constructor(registry, isConstantFunc, path = [], expectedType, scope = new Scope(), errors = []) {
|
|
4748
|
-
this.registry = registry;
|
|
4749
|
-
this.path = path;
|
|
4750
|
-
this.key = path.map((part) => `[${part}]`).join("");
|
|
4751
|
-
this.scope = scope;
|
|
4752
|
-
this.errors = errors;
|
|
4753
|
-
this.expectedType = expectedType;
|
|
4754
|
-
this._isConstant = isConstantFunc;
|
|
4755
|
-
}
|
|
4756
|
-
/**
|
|
4757
|
-
* @param expr the JSON expression to parse
|
|
4758
|
-
* @param index the optional argument index if this expression is an argument of a parent expression that's being parsed
|
|
4759
|
-
* @param options
|
|
4760
|
-
* @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.
|
|
4761
|
-
* @private
|
|
4762
|
-
*/
|
|
4763
|
-
parse(expr, index, expectedType, bindings, options = {}) {
|
|
4764
|
-
if (index) return this.concat(index, expectedType, bindings)._parse(expr, options);
|
|
4765
|
-
return this._parse(expr, options);
|
|
4766
|
-
}
|
|
4767
|
-
_parse(expr, options) {
|
|
4768
|
-
if (expr === null || typeof expr === "string" || typeof expr === "boolean" || typeof expr === "number") expr = ["literal", expr];
|
|
4769
|
-
const key = this.key;
|
|
4770
|
-
function annotate(parsed, type, typeAnnotation) {
|
|
4771
|
-
if (typeAnnotation === "assert") return new Assertion(type, [parsed], key);
|
|
4772
|
-
else if (typeAnnotation === "coerce") return new Coercion(type, [parsed], key);
|
|
4773
|
-
else return parsed;
|
|
4774
|
-
}
|
|
4775
|
-
if (Array.isArray(expr)) {
|
|
4776
|
-
if (expr.length === 0) return this.error("Expected an array with at least one element. If you wanted a literal array, use [\"literal\", []].");
|
|
4777
|
-
const op = expr[0];
|
|
4778
|
-
if (typeof op !== "string") {
|
|
4779
|
-
this.error(`Expression name must be a string, but found ${typeof op} instead. If you wanted a literal array, use ["literal", [...]].`, 0);
|
|
4780
|
-
return null;
|
|
5094
|
+
}
|
|
5095
|
+
throw new RuntimeError(`Could not parse colorArray from value '${typeof input === "string" ? input : JSON.stringify(input)}'`, this.key);
|
|
4781
5096
|
}
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
const actual = parsed.type;
|
|
4789
|
-
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");
|
|
4790
|
-
else if ("projectionDefinition" === expected.kind && ["string", "array"].includes(actual.kind) || [
|
|
4791
|
-
"color",
|
|
4792
|
-
"formatted",
|
|
4793
|
-
"resolvedImage"
|
|
4794
|
-
].includes(expected.kind) && ["value", "string"].includes(actual.kind) || ["padding", "numberArray"].includes(expected.kind) && [
|
|
4795
|
-
"value",
|
|
4796
|
-
"number",
|
|
4797
|
-
"array"
|
|
4798
|
-
].includes(actual.kind) || "colorArray" === expected.kind && [
|
|
4799
|
-
"value",
|
|
4800
|
-
"string",
|
|
4801
|
-
"array"
|
|
4802
|
-
].includes(actual.kind) || "variableAnchorOffsetCollection" === expected.kind && ["value", "array"].includes(actual.kind)) parsed = annotate(parsed, expected, options.typeAnnotation || "coerce");
|
|
4803
|
-
else if (this.checkSubtype(expected, actual)) return null;
|
|
5097
|
+
case "variableAnchorOffsetCollection": {
|
|
5098
|
+
let input;
|
|
5099
|
+
for (const arg of this.args) {
|
|
5100
|
+
input = arg.evaluate(ctx);
|
|
5101
|
+
const coll = VariableAnchorOffsetCollection.parse(input);
|
|
5102
|
+
if (coll) return coll;
|
|
4804
5103
|
}
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
5104
|
+
throw new RuntimeError(`Could not parse variableAnchorOffsetCollection from value '${typeof input === "string" ? input : JSON.stringify(input)}'`, this.key);
|
|
5105
|
+
}
|
|
5106
|
+
case "number": {
|
|
5107
|
+
let value = null;
|
|
5108
|
+
for (const arg of this.args) {
|
|
5109
|
+
value = arg.evaluate(ctx);
|
|
5110
|
+
if (value === null) return 0;
|
|
5111
|
+
const num = Number(value);
|
|
5112
|
+
if (isNaN(num)) continue;
|
|
5113
|
+
return num;
|
|
4813
5114
|
}
|
|
4814
|
-
|
|
5115
|
+
throw new RuntimeError(`Could not convert ${JSON.stringify(value)} to number.`, this.key);
|
|
4815
5116
|
}
|
|
4816
|
-
return
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
* Note that `errors` property, intended for collecting errors while
|
|
4826
|
-
* parsing, is copied by reference rather than cloned.
|
|
4827
|
-
* @private
|
|
4828
|
-
*/
|
|
4829
|
-
concat(index, expectedType, bindings) {
|
|
4830
|
-
const path = typeof index === "number" ? this.path.concat(index) : this.path;
|
|
4831
|
-
const scope = bindings ? this.scope.concat(bindings) : this.scope;
|
|
4832
|
-
return new ParsingContext(this.registry, this._isConstant, path, expectedType || null, scope, this.errors);
|
|
5117
|
+
case "formatted": return Formatted.fromString(valueToString(this.args[0].evaluate(ctx)));
|
|
5118
|
+
case "resolvedImage": return ResolvedImage.fromString(valueToString(this.args[0].evaluate(ctx)));
|
|
5119
|
+
case "projectionDefinition": {
|
|
5120
|
+
const input = this.args[0].evaluate(ctx);
|
|
5121
|
+
if (ProjectionDefinition.parse(input)) return input;
|
|
5122
|
+
throw new RuntimeError(`Could not parse projectionDefinition from value '${typeof input === "string" ? input : JSON.stringify(input)}'`, this.key);
|
|
5123
|
+
}
|
|
5124
|
+
default: return valueToString(this.args[0].evaluate(ctx));
|
|
5125
|
+
}
|
|
4833
5126
|
}
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
* @param error The message
|
|
4837
|
-
* @param keys Optionally specify the source of the error at a child
|
|
4838
|
-
* of the current expression at `this.key`.
|
|
4839
|
-
* @private
|
|
4840
|
-
*/
|
|
4841
|
-
error(error, ...keys) {
|
|
4842
|
-
const key = `${this.key}${keys.map((k) => `[${k}]`).join("")}`;
|
|
4843
|
-
this.errors.push(new ExpressionParsingError(key, error));
|
|
5127
|
+
eachChild(fn) {
|
|
5128
|
+
this.args.forEach(fn);
|
|
4844
5129
|
}
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
* error message and also pushes it to `this.errors`.
|
|
4848
|
-
* @param expected The expected type
|
|
4849
|
-
* @param t The actual type
|
|
4850
|
-
* @returns null if `t` is a subtype of `expected`; otherwise returns an error message
|
|
4851
|
-
*/
|
|
4852
|
-
checkSubtype(expected, t) {
|
|
4853
|
-
const error = checkSubtype(expected, t);
|
|
4854
|
-
if (error) this.error(error);
|
|
4855
|
-
return error;
|
|
5130
|
+
outputDefined() {
|
|
5131
|
+
return this.args.every((arg) => arg.outputDefined());
|
|
4856
5132
|
}
|
|
4857
5133
|
};
|
|
4858
|
-
|
|
5134
|
+
|
|
4859
5135
|
//#region src/expression/definitions/let.ts
|
|
4860
5136
|
var Let = class Let {
|
|
4861
5137
|
constructor(bindings, result) {
|
|
@@ -4889,7 +5165,7 @@
|
|
|
4889
5165
|
return this.result.outputDefined();
|
|
4890
5166
|
}
|
|
4891
5167
|
};
|
|
4892
|
-
|
|
5168
|
+
|
|
4893
5169
|
//#region src/expression/definitions/var.ts
|
|
4894
5170
|
var Var = class Var {
|
|
4895
5171
|
constructor(name, boundExpression) {
|
|
@@ -4911,7 +5187,7 @@
|
|
|
4911
5187
|
return false;
|
|
4912
5188
|
}
|
|
4913
5189
|
};
|
|
4914
|
-
|
|
5190
|
+
|
|
4915
5191
|
//#region src/expression/definitions/at.ts
|
|
4916
5192
|
var At = class At {
|
|
4917
5193
|
constructor(type, index, input, key) {
|
|
@@ -4944,7 +5220,7 @@
|
|
|
4944
5220
|
return false;
|
|
4945
5221
|
}
|
|
4946
5222
|
};
|
|
4947
|
-
|
|
5223
|
+
|
|
4948
5224
|
//#region src/expression/definitions/in.ts
|
|
4949
5225
|
var In = class In {
|
|
4950
5226
|
constructor(needle, haystack, key) {
|
|
@@ -4988,7 +5264,7 @@
|
|
|
4988
5264
|
return true;
|
|
4989
5265
|
}
|
|
4990
5266
|
};
|
|
4991
|
-
|
|
5267
|
+
|
|
4992
5268
|
//#region src/expression/definitions/index_of.ts
|
|
4993
5269
|
var IndexOf = class IndexOf {
|
|
4994
5270
|
constructor(needle, haystack, key, fromIndex) {
|
|
@@ -5043,7 +5319,7 @@
|
|
|
5043
5319
|
return false;
|
|
5044
5320
|
}
|
|
5045
5321
|
};
|
|
5046
|
-
|
|
5322
|
+
|
|
5047
5323
|
//#region src/expression/definitions/match.ts
|
|
5048
5324
|
var Match = class Match {
|
|
5049
5325
|
constructor(inputType, outputType, input, cases, outputs, otherwise) {
|
|
@@ -5102,7 +5378,7 @@
|
|
|
5102
5378
|
return this.outputs.every((out) => out.outputDefined()) && this.otherwise.outputDefined();
|
|
5103
5379
|
}
|
|
5104
5380
|
};
|
|
5105
|
-
|
|
5381
|
+
|
|
5106
5382
|
//#region src/expression/definitions/case.ts
|
|
5107
5383
|
var Case = class Case {
|
|
5108
5384
|
constructor(type, branches, otherwise) {
|
|
@@ -5144,324 +5420,51 @@
|
|
|
5144
5420
|
return this.branches.every(([_, out]) => out.outputDefined()) && this.otherwise.outputDefined();
|
|
5145
5421
|
}
|
|
5146
5422
|
};
|
|
5147
|
-
|
|
5423
|
+
|
|
5148
5424
|
//#region src/expression/definitions/slice.ts
|
|
5149
5425
|
var Slice = class Slice {
|
|
5150
|
-
constructor(type, input, beginIndex, key, endIndex) {
|
|
5151
|
-
this.type = type;
|
|
5152
|
-
this.input = input;
|
|
5153
|
-
this.beginIndex = beginIndex;
|
|
5154
|
-
this.key = key;
|
|
5155
|
-
this.endIndex = endIndex;
|
|
5156
|
-
}
|
|
5157
|
-
static parse(args, context) {
|
|
5158
|
-
if (args.length <= 2 || args.length >= 5) return context.error(`Expected 2 or 3 arguments, but found ${args.length - 1} instead.`);
|
|
5159
|
-
const input = context.parse(args[1], 1, ValueType);
|
|
5160
|
-
const beginIndex = context.parse(args[2], 2, NumberType);
|
|
5161
|
-
if (!input || !beginIndex) return null;
|
|
5162
|
-
if (!isValidType(input.type, [
|
|
5163
|
-
array(ValueType),
|
|
5164
|
-
StringType,
|
|
5165
|
-
ValueType
|
|
5166
|
-
])) return context.error(`Expected first argument to be of type array or string, but found ${typeToString(input.type)} instead`);
|
|
5167
|
-
if (args.length === 4) {
|
|
5168
|
-
const endIndex = context.parse(args[3], 3, NumberType);
|
|
5169
|
-
if (!endIndex) return null;
|
|
5170
|
-
return new Slice(input.type, input, beginIndex, context.key, endIndex);
|
|
5171
|
-
} else return new Slice(input.type, input, beginIndex, context.key);
|
|
5172
|
-
}
|
|
5173
|
-
evaluate(ctx) {
|
|
5174
|
-
const input = this.input.evaluate(ctx);
|
|
5175
|
-
const beginIndex = this.beginIndex.evaluate(ctx);
|
|
5176
|
-
let endIndex;
|
|
5177
|
-
if (this.endIndex) endIndex = this.endIndex.evaluate(ctx);
|
|
5178
|
-
if (isValidNativeType(input, ["string"])) return [...input].slice(beginIndex, endIndex).join("");
|
|
5179
|
-
else if (isValidNativeType(input, ["array"])) return input.slice(beginIndex, endIndex);
|
|
5180
|
-
else throw new RuntimeError(`Expected first argument to be of type array or string, but found ${typeToString(typeOf(input))} instead.`, this.key);
|
|
5181
|
-
}
|
|
5182
|
-
eachChild(fn) {
|
|
5183
|
-
fn(this.input);
|
|
5184
|
-
fn(this.beginIndex);
|
|
5185
|
-
if (this.endIndex) fn(this.endIndex);
|
|
5186
|
-
}
|
|
5187
|
-
outputDefined() {
|
|
5188
|
-
return false;
|
|
5189
|
-
}
|
|
5190
|
-
};
|
|
5191
|
-
//#endregion
|
|
5192
|
-
//#region src/expression/stops.ts
|
|
5193
|
-
/**
|
|
5194
|
-
* Returns the index of the last stop <= input, or 0 if it doesn't exist.
|
|
5195
|
-
* @private
|
|
5196
|
-
*/
|
|
5197
|
-
function findStopLessThanOrEqualTo(stops, input, key) {
|
|
5198
|
-
const lastIndex = stops.length - 1;
|
|
5199
|
-
let lowerIndex = 0;
|
|
5200
|
-
let upperIndex = lastIndex;
|
|
5201
|
-
let currentIndex = 0;
|
|
5202
|
-
let currentValue, nextValue;
|
|
5203
|
-
while (lowerIndex <= upperIndex) {
|
|
5204
|
-
currentIndex = Math.floor((lowerIndex + upperIndex) / 2);
|
|
5205
|
-
currentValue = stops[currentIndex];
|
|
5206
|
-
nextValue = stops[currentIndex + 1];
|
|
5207
|
-
if (currentValue <= input) {
|
|
5208
|
-
if (currentIndex === lastIndex || input < nextValue) return currentIndex;
|
|
5209
|
-
lowerIndex = currentIndex + 1;
|
|
5210
|
-
} else if (currentValue > input) upperIndex = currentIndex - 1;
|
|
5211
|
-
else throw new RuntimeError("Input is not a number.", key);
|
|
5212
|
-
}
|
|
5213
|
-
return 0;
|
|
5214
|
-
}
|
|
5215
|
-
//#endregion
|
|
5216
|
-
//#region src/expression/definitions/step.ts
|
|
5217
|
-
var Step = class Step {
|
|
5218
|
-
constructor(type, input, stops, key) {
|
|
5219
|
-
this.type = type;
|
|
5220
|
-
this.input = input;
|
|
5221
|
-
this.key = key;
|
|
5222
|
-
this.labels = [];
|
|
5223
|
-
this.outputs = [];
|
|
5224
|
-
for (const [label, expression] of stops) {
|
|
5225
|
-
this.labels.push(label);
|
|
5226
|
-
this.outputs.push(expression);
|
|
5227
|
-
}
|
|
5228
|
-
}
|
|
5229
|
-
static parse(args, context) {
|
|
5230
|
-
if (args.length - 1 < 4) return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`);
|
|
5231
|
-
if ((args.length - 1) % 2 !== 0) return context.error("Expected an even number of arguments.");
|
|
5232
|
-
const input = context.parse(args[1], 1, NumberType);
|
|
5233
|
-
if (!input) return null;
|
|
5234
|
-
const stops = [];
|
|
5235
|
-
let outputType = null;
|
|
5236
|
-
if (context.expectedType && context.expectedType.kind !== "value") outputType = context.expectedType;
|
|
5237
|
-
for (let i = 1; i < args.length; i += 2) {
|
|
5238
|
-
const label = i === 1 ? -Infinity : args[i];
|
|
5239
|
-
const value = args[i + 1];
|
|
5240
|
-
const labelKey = i;
|
|
5241
|
-
const valueKey = i + 1;
|
|
5242
|
-
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);
|
|
5243
|
-
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);
|
|
5244
|
-
const parsed = context.parse(value, valueKey, outputType);
|
|
5245
|
-
if (!parsed) return null;
|
|
5246
|
-
outputType = outputType || parsed.type;
|
|
5247
|
-
stops.push([label, parsed]);
|
|
5248
|
-
}
|
|
5249
|
-
return new Step(outputType, input, stops, context.key);
|
|
5250
|
-
}
|
|
5251
|
-
evaluate(ctx) {
|
|
5252
|
-
const labels = this.labels;
|
|
5253
|
-
const outputs = this.outputs;
|
|
5254
|
-
if (labels.length === 1) return outputs[0].evaluate(ctx);
|
|
5255
|
-
const value = this.input.evaluate(ctx);
|
|
5256
|
-
if (value <= labels[0]) return outputs[0].evaluate(ctx);
|
|
5257
|
-
const stopCount = labels.length;
|
|
5258
|
-
if (value >= labels[stopCount - 1]) return outputs[stopCount - 1].evaluate(ctx);
|
|
5259
|
-
return outputs[findStopLessThanOrEqualTo(labels, value, this.key)].evaluate(ctx);
|
|
5260
|
-
}
|
|
5261
|
-
eachChild(fn) {
|
|
5262
|
-
fn(this.input);
|
|
5263
|
-
for (const expression of this.outputs) fn(expression);
|
|
5264
|
-
}
|
|
5265
|
-
outputDefined() {
|
|
5266
|
-
return this.outputs.every((out) => out.outputDefined());
|
|
5267
|
-
}
|
|
5268
|
-
};
|
|
5269
|
-
//#endregion
|
|
5270
|
-
//#region node_modules/@mapbox/unitbezier/index.js
|
|
5271
|
-
function unitBezier(p1x, p1y, p2x, p2y) {
|
|
5272
|
-
const cx = 3 * p1x;
|
|
5273
|
-
const bx = 3 * (p2x - p1x) - cx;
|
|
5274
|
-
const ax = 1 - cx - bx;
|
|
5275
|
-
const cy = 3 * p1y;
|
|
5276
|
-
const by = 3 * (p2y - p1y) - cy;
|
|
5277
|
-
const ay = 1 - cy - by;
|
|
5278
|
-
return function solve(x, epsilon = 1e-6) {
|
|
5279
|
-
if (x <= 0) return 0;
|
|
5280
|
-
if (x >= 1) return 1;
|
|
5281
|
-
let t = x;
|
|
5282
|
-
for (let i = 0; i < 8; i++) {
|
|
5283
|
-
const x2 = ((ax * t + bx) * t + cx) * t - x;
|
|
5284
|
-
if (Math.abs(x2) < epsilon) return ((ay * t + by) * t + cy) * t;
|
|
5285
|
-
const d2 = (3 * ax * t + 2 * bx) * t + cx;
|
|
5286
|
-
if (Math.abs(d2) < 1e-6) break;
|
|
5287
|
-
t -= x2 / d2;
|
|
5288
|
-
}
|
|
5289
|
-
let t0 = 0;
|
|
5290
|
-
let t1 = 1;
|
|
5291
|
-
t = x;
|
|
5292
|
-
for (let i = 0; i < 20; i++) {
|
|
5293
|
-
const x2 = ((ax * t + bx) * t + cx) * t;
|
|
5294
|
-
if (Math.abs(x2 - x) < epsilon) break;
|
|
5295
|
-
if (x > x2) t0 = t;
|
|
5296
|
-
else t1 = t;
|
|
5297
|
-
t = (t0 + t1) * .5;
|
|
5298
|
-
}
|
|
5299
|
-
return ((ay * t + by) * t + cy) * t;
|
|
5300
|
-
};
|
|
5301
|
-
}
|
|
5302
|
-
//#endregion
|
|
5303
|
-
//#region src/expression/definitions/interpolate.ts
|
|
5304
|
-
var Interpolate = class Interpolate {
|
|
5305
|
-
constructor(type, operator, interpolation, input, stops, key) {
|
|
5426
|
+
constructor(type, input, beginIndex, key, endIndex) {
|
|
5306
5427
|
this.type = type;
|
|
5307
|
-
this.operator = operator;
|
|
5308
|
-
this.interpolation = interpolation;
|
|
5309
5428
|
this.input = input;
|
|
5429
|
+
this.beginIndex = beginIndex;
|
|
5310
5430
|
this.key = key;
|
|
5311
|
-
this.
|
|
5312
|
-
this.outputs = [];
|
|
5313
|
-
for (const [label, expression] of stops) {
|
|
5314
|
-
this.labels.push(label);
|
|
5315
|
-
this.outputs.push(expression);
|
|
5316
|
-
}
|
|
5317
|
-
}
|
|
5318
|
-
static interpolationFactor(interpolation, input, lower, upper) {
|
|
5319
|
-
let t = 0;
|
|
5320
|
-
if (interpolation.name === "exponential") t = exponentialInterpolation(input, interpolation.base, lower, upper);
|
|
5321
|
-
else if (interpolation.name === "linear") t = exponentialInterpolation(input, 1, lower, upper);
|
|
5322
|
-
else if (interpolation.name === "cubic-bezier") {
|
|
5323
|
-
const c = interpolation.controlPoints;
|
|
5324
|
-
t = unitBezier(c[0], c[1], c[2], c[3])(exponentialInterpolation(input, 1, lower, upper));
|
|
5325
|
-
}
|
|
5326
|
-
return t;
|
|
5431
|
+
this.endIndex = endIndex;
|
|
5327
5432
|
}
|
|
5328
5433
|
static parse(args, context) {
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
name: "cubic-bezier",
|
|
5344
|
-
controlPoints
|
|
5345
|
-
};
|
|
5346
|
-
} else return context.error(`Unknown interpolation type ${String(interpolation[0])}`, 1, 0);
|
|
5347
|
-
if (args.length - 1 < 4) return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`);
|
|
5348
|
-
if ((args.length - 1) % 2 !== 0) return context.error("Expected an even number of arguments.");
|
|
5349
|
-
input = context.parse(input, 2, NumberType);
|
|
5350
|
-
if (!input) return null;
|
|
5351
|
-
const stops = [];
|
|
5352
|
-
let outputType = null;
|
|
5353
|
-
if ((operator === "interpolate-hcl" || operator === "interpolate-lab") && context.expectedType != ColorArrayType) outputType = ColorType;
|
|
5354
|
-
else if (context.expectedType && context.expectedType.kind !== "value") outputType = context.expectedType;
|
|
5355
|
-
for (let i = 0; i < rest.length; i += 2) {
|
|
5356
|
-
const label = rest[i];
|
|
5357
|
-
const value = rest[i + 1];
|
|
5358
|
-
const labelKey = i + 3;
|
|
5359
|
-
const valueKey = i + 4;
|
|
5360
|
-
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);
|
|
5361
|
-
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);
|
|
5362
|
-
const parsed = context.parse(value, valueKey, outputType);
|
|
5363
|
-
if (!parsed) return null;
|
|
5364
|
-
outputType = outputType || parsed.type;
|
|
5365
|
-
stops.push([label, parsed]);
|
|
5366
|
-
}
|
|
5367
|
-
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.`);
|
|
5368
|
-
return new Interpolate(outputType, operator, interpolation, input, stops, context.key);
|
|
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);
|
|
5369
5448
|
}
|
|
5370
5449
|
evaluate(ctx) {
|
|
5371
|
-
const
|
|
5372
|
-
const
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
if (
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
const index = findStopLessThanOrEqualTo(labels, value, this.key);
|
|
5379
|
-
const lower = labels[index];
|
|
5380
|
-
const upper = labels[index + 1];
|
|
5381
|
-
const t = Interpolate.interpolationFactor(this.interpolation, value, lower, upper);
|
|
5382
|
-
const outputLower = outputs[index].evaluate(ctx);
|
|
5383
|
-
const outputUpper = outputs[index + 1].evaluate(ctx);
|
|
5384
|
-
switch (this.operator) {
|
|
5385
|
-
case "interpolate": switch (this.type.kind) {
|
|
5386
|
-
case "number": return interpolateNumber(outputLower, outputUpper, t);
|
|
5387
|
-
case "color": return Color$1.interpolate(outputLower, outputUpper, t);
|
|
5388
|
-
case "padding": return Padding.interpolate(outputLower, outputUpper, t);
|
|
5389
|
-
case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t);
|
|
5390
|
-
case "numberArray": return NumberArray.interpolate(outputLower, outputUpper, t);
|
|
5391
|
-
case "variableAnchorOffsetCollection": return VariableAnchorOffsetCollection.interpolate(outputLower, outputUpper, t, this.key);
|
|
5392
|
-
case "array": return interpolateArray(outputLower, outputUpper, t);
|
|
5393
|
-
case "projectionDefinition": return ProjectionDefinition.interpolate(outputLower, outputUpper, t);
|
|
5394
|
-
}
|
|
5395
|
-
case "interpolate-hcl": switch (this.type.kind) {
|
|
5396
|
-
case "color": return Color$1.interpolate(outputLower, outputUpper, t, "hcl");
|
|
5397
|
-
case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t, "hcl");
|
|
5398
|
-
}
|
|
5399
|
-
case "interpolate-lab": switch (this.type.kind) {
|
|
5400
|
-
case "color": return Color$1.interpolate(outputLower, outputUpper, t, "lab");
|
|
5401
|
-
case "colorArray": return ColorArray.interpolate(outputLower, outputUpper, t, "lab");
|
|
5402
|
-
}
|
|
5403
|
-
}
|
|
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);
|
|
5404
5457
|
}
|
|
5405
5458
|
eachChild(fn) {
|
|
5406
5459
|
fn(this.input);
|
|
5407
|
-
|
|
5460
|
+
fn(this.beginIndex);
|
|
5461
|
+
if (this.endIndex) fn(this.endIndex);
|
|
5408
5462
|
}
|
|
5409
5463
|
outputDefined() {
|
|
5410
|
-
return
|
|
5464
|
+
return false;
|
|
5411
5465
|
}
|
|
5412
5466
|
};
|
|
5413
|
-
|
|
5414
|
-
* Returns a ratio that can be used to interpolate between exponential function
|
|
5415
|
-
* stops.
|
|
5416
|
-
* 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,
|
|
5417
|
-
* and `a` and `b` are constants affording sufficient degrees of freedom to fit
|
|
5418
|
-
* the function to the given stops.
|
|
5419
|
-
*
|
|
5420
|
-
* Here's a bit of algebra that lets us compute `f(x)` directly from the stop
|
|
5421
|
-
* values without explicitly solving for `a` and `b`:
|
|
5422
|
-
*
|
|
5423
|
-
* First stop value: `f(x0) = y0 = a * base^x0 + b`
|
|
5424
|
-
* Second stop value: `f(x1) = y1 = a * base^x1 + b`
|
|
5425
|
-
* => `y1 - y0 = a(base^x1 - base^x0)`
|
|
5426
|
-
* => `a = (y1 - y0)/(base^x1 - base^x0)`
|
|
5427
|
-
*
|
|
5428
|
-
* Desired value: `f(x) = y = a * base^x + b`
|
|
5429
|
-
* => `f(x) = y0 + a * (base^x - base^x0)`
|
|
5430
|
-
*
|
|
5431
|
-
* From the above, we can replace the `a` in `a * (base^x - base^x0)` and do a
|
|
5432
|
-
* little algebra:
|
|
5433
|
-
* ```
|
|
5434
|
-
* a * (base^x - base^x0) = (y1 - y0)/(base^x1 - base^x0) * (base^x - base^x0)
|
|
5435
|
-
* = (y1 - y0) * (base^x - base^x0) / (base^x1 - base^x0)
|
|
5436
|
-
* ```
|
|
5437
|
-
*
|
|
5438
|
-
* If we let `(base^x - base^x0) / (base^x1 base^x0)`, then we have
|
|
5439
|
-
* `f(x) = y0 + (y1 - y0) * ratio`. In other words, `ratio` may be treated as
|
|
5440
|
-
* an interpolation factor between the two stops' output values.
|
|
5441
|
-
*
|
|
5442
|
-
* (Note: a slightly different form for `ratio`,
|
|
5443
|
-
* `(base^(x-x0) - 1) / (base^(x1-x0) - 1) `, is equivalent, but requires fewer
|
|
5444
|
-
* expensive `Math.pow()` operations.)
|
|
5445
|
-
*
|
|
5446
|
-
* @private
|
|
5447
|
-
*/
|
|
5448
|
-
function exponentialInterpolation(input, base, lowerValue, upperValue) {
|
|
5449
|
-
const difference = upperValue - lowerValue;
|
|
5450
|
-
const progress = input - lowerValue;
|
|
5451
|
-
if (difference === 0) return 0;
|
|
5452
|
-
else if (base === 1) return progress / difference;
|
|
5453
|
-
else return (Math.pow(base, progress) - 1) / (Math.pow(base, difference) - 1);
|
|
5454
|
-
}
|
|
5455
|
-
const interpolateFactory = {
|
|
5456
|
-
color: Color$1.interpolate,
|
|
5457
|
-
number: interpolateNumber,
|
|
5458
|
-
padding: Padding.interpolate,
|
|
5459
|
-
numberArray: NumberArray.interpolate,
|
|
5460
|
-
colorArray: ColorArray.interpolate,
|
|
5461
|
-
variableAnchorOffsetCollection: VariableAnchorOffsetCollection.interpolate,
|
|
5462
|
-
array: interpolateArray
|
|
5463
|
-
};
|
|
5464
|
-
//#endregion
|
|
5467
|
+
|
|
5465
5468
|
//#region src/expression/definitions/coalesce.ts
|
|
5466
5469
|
var Coalesce = class Coalesce {
|
|
5467
5470
|
constructor(type, args) {
|
|
@@ -5506,7 +5509,7 @@
|
|
|
5506
5509
|
return this.args.every((arg) => arg.outputDefined());
|
|
5507
5510
|
}
|
|
5508
5511
|
};
|
|
5509
|
-
|
|
5512
|
+
|
|
5510
5513
|
//#region src/expression/definitions/comparison.ts
|
|
5511
5514
|
function isComparableType(op, type) {
|
|
5512
5515
|
if (op === "==" || op === "!=") return type.kind === "boolean" || type.kind === "string" || type.kind === "number" || type.kind === "null" || type.kind === "value";
|
|
@@ -5629,7 +5632,7 @@
|
|
|
5629
5632
|
const GreaterThan = makeComparison(">", gt, gtCollate);
|
|
5630
5633
|
const LessThanOrEqual = makeComparison("<=", lteq, lteqCollate);
|
|
5631
5634
|
const GreaterThanOrEqual = makeComparison(">=", gteq, gteqCollate);
|
|
5632
|
-
|
|
5635
|
+
|
|
5633
5636
|
//#region src/expression/definitions/collator.ts
|
|
5634
5637
|
var CollatorExpression = class CollatorExpression {
|
|
5635
5638
|
constructor(caseSensitive, diacriticSensitive, locale) {
|
|
@@ -5665,7 +5668,7 @@
|
|
|
5665
5668
|
return false;
|
|
5666
5669
|
}
|
|
5667
5670
|
};
|
|
5668
|
-
|
|
5671
|
+
|
|
5669
5672
|
//#region src/expression/definitions/number_format.ts
|
|
5670
5673
|
var NumberFormat = class NumberFormat {
|
|
5671
5674
|
constructor(number, locale, currency, unit, minFractionDigits, maxFractionDigits) {
|
|
@@ -5717,103 +5720,22 @@
|
|
|
5717
5720
|
currency: this.currency ? this.currency.evaluate(ctx) : void 0,
|
|
5718
5721
|
unit: this.unit ? this.unit.evaluate(ctx) : void 0,
|
|
5719
5722
|
minimumFractionDigits: this.minFractionDigits ? this.minFractionDigits.evaluate(ctx) : void 0,
|
|
5720
|
-
maximumFractionDigits: this.maxFractionDigits ? this.maxFractionDigits.evaluate(ctx) : void 0
|
|
5721
|
-
}).format(this.number.evaluate(ctx));
|
|
5722
|
-
}
|
|
5723
|
-
eachChild(fn) {
|
|
5724
|
-
fn(this.number);
|
|
5725
|
-
if (this.locale) fn(this.locale);
|
|
5726
|
-
if (this.currency) fn(this.currency);
|
|
5727
|
-
if (this.unit) fn(this.unit);
|
|
5728
|
-
if (this.minFractionDigits) fn(this.minFractionDigits);
|
|
5729
|
-
if (this.maxFractionDigits) fn(this.maxFractionDigits);
|
|
5730
|
-
}
|
|
5731
|
-
outputDefined() {
|
|
5732
|
-
return false;
|
|
5733
|
-
}
|
|
5734
|
-
};
|
|
5735
|
-
//#endregion
|
|
5736
|
-
//#region src/expression/definitions/format.ts
|
|
5737
|
-
var FormatExpression = class FormatExpression {
|
|
5738
|
-
constructor(sections) {
|
|
5739
|
-
this.type = FormattedType;
|
|
5740
|
-
this.sections = sections;
|
|
5741
|
-
}
|
|
5742
|
-
static parse(args, context) {
|
|
5743
|
-
if (args.length < 2) return context.error("Expected at least one argument.");
|
|
5744
|
-
const firstArg = args[1];
|
|
5745
|
-
if (!Array.isArray(firstArg) && typeof firstArg === "object") return context.error("First argument must be an image or text section.");
|
|
5746
|
-
const sections = [];
|
|
5747
|
-
let nextTokenMayBeObject = false;
|
|
5748
|
-
for (let i = 1; i <= args.length - 1; ++i) {
|
|
5749
|
-
const arg = args[i];
|
|
5750
|
-
if (nextTokenMayBeObject && typeof arg === "object" && !Array.isArray(arg)) {
|
|
5751
|
-
nextTokenMayBeObject = false;
|
|
5752
|
-
let scale = null;
|
|
5753
|
-
if (arg["font-scale"]) {
|
|
5754
|
-
scale = context.parse(arg["font-scale"], 1, NumberType);
|
|
5755
|
-
if (!scale) return null;
|
|
5756
|
-
}
|
|
5757
|
-
let font = null;
|
|
5758
|
-
if (arg["text-font"]) {
|
|
5759
|
-
font = context.parse(arg["text-font"], 1, array(StringType));
|
|
5760
|
-
if (!font) return null;
|
|
5761
|
-
}
|
|
5762
|
-
let textColor = null;
|
|
5763
|
-
if (arg["text-color"]) {
|
|
5764
|
-
textColor = context.parse(arg["text-color"], 1, ColorType);
|
|
5765
|
-
if (!textColor) return null;
|
|
5766
|
-
}
|
|
5767
|
-
let verticalAlign = null;
|
|
5768
|
-
if (arg["vertical-align"]) {
|
|
5769
|
-
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.`);
|
|
5770
|
-
verticalAlign = context.parse(arg["vertical-align"], 1, StringType);
|
|
5771
|
-
if (!verticalAlign) return null;
|
|
5772
|
-
}
|
|
5773
|
-
const lastExpression = sections[sections.length - 1];
|
|
5774
|
-
lastExpression.scale = scale;
|
|
5775
|
-
lastExpression.font = font;
|
|
5776
|
-
lastExpression.textColor = textColor;
|
|
5777
|
-
lastExpression.verticalAlign = verticalAlign;
|
|
5778
|
-
} else {
|
|
5779
|
-
const content = context.parse(args[i], 1, ValueType);
|
|
5780
|
-
if (!content) return null;
|
|
5781
|
-
const kind = content.type.kind;
|
|
5782
|
-
if (kind !== "string" && kind !== "value" && kind !== "null" && kind !== "resolvedImage") return context.error("Formatted text type must be 'string', 'value', 'image' or 'null'.");
|
|
5783
|
-
nextTokenMayBeObject = true;
|
|
5784
|
-
sections.push({
|
|
5785
|
-
content,
|
|
5786
|
-
scale: null,
|
|
5787
|
-
font: null,
|
|
5788
|
-
textColor: null,
|
|
5789
|
-
verticalAlign: null
|
|
5790
|
-
});
|
|
5791
|
-
}
|
|
5792
|
-
}
|
|
5793
|
-
return new FormatExpression(sections);
|
|
5794
|
-
}
|
|
5795
|
-
evaluate(ctx) {
|
|
5796
|
-
const evaluateSection = (section) => {
|
|
5797
|
-
const evaluatedContent = section.content.evaluate(ctx);
|
|
5798
|
-
if (typeOf(evaluatedContent) === ResolvedImageType) return new FormattedSection("", evaluatedContent, null, null, null, section.verticalAlign ? section.verticalAlign.evaluate(ctx) : null);
|
|
5799
|
-
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);
|
|
5800
|
-
};
|
|
5801
|
-
return new Formatted(this.sections.map(evaluateSection));
|
|
5723
|
+
maximumFractionDigits: this.maxFractionDigits ? this.maxFractionDigits.evaluate(ctx) : void 0
|
|
5724
|
+
}).format(this.number.evaluate(ctx));
|
|
5802
5725
|
}
|
|
5803
5726
|
eachChild(fn) {
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
}
|
|
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);
|
|
5811
5733
|
}
|
|
5812
5734
|
outputDefined() {
|
|
5813
5735
|
return false;
|
|
5814
5736
|
}
|
|
5815
5737
|
};
|
|
5816
|
-
|
|
5738
|
+
|
|
5817
5739
|
//#region src/expression/definitions/image.ts
|
|
5818
5740
|
var ImageExpression = class ImageExpression {
|
|
5819
5741
|
constructor(input) {
|
|
@@ -5839,7 +5761,7 @@
|
|
|
5839
5761
|
return false;
|
|
5840
5762
|
}
|
|
5841
5763
|
};
|
|
5842
|
-
|
|
5764
|
+
|
|
5843
5765
|
//#region src/expression/definitions/length.ts
|
|
5844
5766
|
var Length = class Length {
|
|
5845
5767
|
constructor(input, key) {
|
|
@@ -5867,7 +5789,7 @@
|
|
|
5867
5789
|
return false;
|
|
5868
5790
|
}
|
|
5869
5791
|
};
|
|
5870
|
-
|
|
5792
|
+
|
|
5871
5793
|
//#region src/util/geometry_util.ts
|
|
5872
5794
|
const EXTENT = 8192;
|
|
5873
5795
|
function getTileCoordinates(p, canonical) {
|
|
@@ -5963,7 +5885,7 @@
|
|
|
5963
5885
|
if (det1 > 0 && det2 < 0 || det1 < 0 && det2 > 0) return true;
|
|
5964
5886
|
return false;
|
|
5965
5887
|
}
|
|
5966
|
-
|
|
5888
|
+
|
|
5967
5889
|
//#region src/expression/definitions/within.ts
|
|
5968
5890
|
function getTilePolygon(coordinates, bbox, canonical) {
|
|
5969
5891
|
const polygon = [];
|
|
@@ -6125,7 +6047,7 @@
|
|
|
6125
6047
|
return true;
|
|
6126
6048
|
}
|
|
6127
6049
|
};
|
|
6128
|
-
|
|
6050
|
+
|
|
6129
6051
|
//#region node_modules/tinyqueue/index.js
|
|
6130
6052
|
var TinyQueue = class {
|
|
6131
6053
|
constructor(data = [], compare = (a, b) => a < b ? -1 : a > b ? 1 : 0) {
|
|
@@ -6178,54 +6100,10 @@
|
|
|
6178
6100
|
data[pos] = item;
|
|
6179
6101
|
}
|
|
6180
6102
|
};
|
|
6181
|
-
|
|
6182
|
-
//#region src/util/classify_rings.ts
|
|
6183
|
-
/**
|
|
6184
|
-
* Classifies an array of rings into polygons with outer rings and holes
|
|
6185
|
-
* @param rings - the rings to classify
|
|
6186
|
-
* @param maxRings - the maximum number of rings to include in a polygon, use 0 to include all rings
|
|
6187
|
-
* @returns an array of polygons with internal rings as holes
|
|
6188
|
-
*/
|
|
6189
|
-
function classifyRings$1(rings, maxRings) {
|
|
6190
|
-
if (rings.length <= 1) return [rings];
|
|
6191
|
-
const polygons = [];
|
|
6192
|
-
let polygon;
|
|
6193
|
-
let ccw;
|
|
6194
|
-
for (const ring of rings) {
|
|
6195
|
-
const area = calculateSignedArea(ring);
|
|
6196
|
-
if (area === 0) continue;
|
|
6197
|
-
ring.area = Math.abs(area);
|
|
6198
|
-
if (ccw === void 0) ccw = area < 0;
|
|
6199
|
-
if (ccw === area < 0) {
|
|
6200
|
-
if (polygon) polygons.push(polygon);
|
|
6201
|
-
polygon = [ring];
|
|
6202
|
-
} else polygon.push(ring);
|
|
6203
|
-
}
|
|
6204
|
-
if (polygon) polygons.push(polygon);
|
|
6205
|
-
return polygons;
|
|
6206
|
-
}
|
|
6207
|
-
/**
|
|
6208
|
-
* Returns the signed area for the polygon ring. Positive areas are exterior rings and
|
|
6209
|
-
* have a clockwise winding. Negative areas are interior rings and have a counter clockwise
|
|
6210
|
-
* ordering.
|
|
6211
|
-
*
|
|
6212
|
-
* @param ring - Exterior or interior ring
|
|
6213
|
-
* @returns Signed area
|
|
6214
|
-
*/
|
|
6215
|
-
function calculateSignedArea(ring) {
|
|
6216
|
-
let sum = 0;
|
|
6217
|
-
for (let i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) {
|
|
6218
|
-
p1 = ring[i];
|
|
6219
|
-
p2 = ring[j];
|
|
6220
|
-
sum += (p2.x - p1.x) * (p1.y + p2.y);
|
|
6221
|
-
}
|
|
6222
|
-
return sum;
|
|
6223
|
-
}
|
|
6224
|
-
//#endregion
|
|
6103
|
+
|
|
6225
6104
|
//#region src/util/cheap_ruler.ts
|
|
6226
6105
|
const RE = 6378.137;
|
|
6227
|
-
const
|
|
6228
|
-
const E2 = FE * (2 - FE);
|
|
6106
|
+
const E2 = .0066943799901413165;
|
|
6229
6107
|
const RAD = Math.PI / 180;
|
|
6230
6108
|
var CheapRuler = class {
|
|
6231
6109
|
constructor(lat) {
|
|
@@ -6234,7 +6112,7 @@
|
|
|
6234
6112
|
const w2 = 1 / (1 - E2 * (1 - coslat * coslat));
|
|
6235
6113
|
const w = Math.sqrt(w2);
|
|
6236
6114
|
this.kx = m * w * coslat;
|
|
6237
|
-
this.ky = m * w * w2 *
|
|
6115
|
+
this.ky = m * w * w2 * .9933056200098587;
|
|
6238
6116
|
}
|
|
6239
6117
|
/**
|
|
6240
6118
|
* Given two points of the form [longitude, latitude], returns the distance.
|
|
@@ -6305,7 +6183,7 @@
|
|
|
6305
6183
|
return deg;
|
|
6306
6184
|
}
|
|
6307
6185
|
};
|
|
6308
|
-
|
|
6186
|
+
|
|
6309
6187
|
//#region src/expression/definitions/distance.ts
|
|
6310
6188
|
const MinPointsSize = 100;
|
|
6311
6189
|
const MinLinePointsSize = 50;
|
|
@@ -6573,9 +6451,7 @@
|
|
|
6573
6451
|
case "LineString":
|
|
6574
6452
|
dist = Math.min(dist, pointSetToPointSetDistance(pointPosition, false, geometry.coordinates, true, ruler, dist));
|
|
6575
6453
|
break;
|
|
6576
|
-
case "Polygon":
|
|
6577
|
-
dist = Math.min(dist, pointsToPolygonDistance(pointPosition, false, geometry.coordinates, ruler, dist));
|
|
6578
|
-
break;
|
|
6454
|
+
case "Polygon": dist = Math.min(dist, pointsToPolygonDistance(pointPosition, false, geometry.coordinates, ruler, dist));
|
|
6579
6455
|
}
|
|
6580
6456
|
if (dist === 0) return dist;
|
|
6581
6457
|
}
|
|
@@ -6595,9 +6471,7 @@
|
|
|
6595
6471
|
case "LineString":
|
|
6596
6472
|
dist = Math.min(dist, pointSetToPointSetDistance(linePositions, true, geometry.coordinates, true, ruler, dist));
|
|
6597
6473
|
break;
|
|
6598
|
-
case "Polygon":
|
|
6599
|
-
dist = Math.min(dist, pointsToPolygonDistance(linePositions, true, geometry.coordinates, ruler, dist));
|
|
6600
|
-
break;
|
|
6474
|
+
case "Polygon": dist = Math.min(dist, pointsToPolygonDistance(linePositions, true, geometry.coordinates, ruler, dist));
|
|
6601
6475
|
}
|
|
6602
6476
|
if (dist === 0) return dist;
|
|
6603
6477
|
}
|
|
@@ -6621,9 +6495,7 @@
|
|
|
6621
6495
|
case "LineString":
|
|
6622
6496
|
dist = Math.min(dist, pointsToPolygonDistance(geometry.coordinates, true, polygon, ruler, dist));
|
|
6623
6497
|
break;
|
|
6624
|
-
case "Polygon":
|
|
6625
|
-
dist = Math.min(dist, polygonToPolygonDistance(polygon, geometry.coordinates, ruler, dist));
|
|
6626
|
-
break;
|
|
6498
|
+
case "Polygon": dist = Math.min(dist, polygonToPolygonDistance(polygon, geometry.coordinates, ruler, dist));
|
|
6627
6499
|
}
|
|
6628
6500
|
if (dist === 0) return dist;
|
|
6629
6501
|
}
|
|
@@ -6679,7 +6551,48 @@
|
|
|
6679
6551
|
return true;
|
|
6680
6552
|
}
|
|
6681
6553
|
};
|
|
6682
|
-
|
|
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
|
+
|
|
6683
6596
|
//#region src/expression/definitions/global_state.ts
|
|
6684
6597
|
var GlobalState = class GlobalState {
|
|
6685
6598
|
constructor(key) {
|
|
@@ -6703,7 +6616,7 @@
|
|
|
6703
6616
|
return false;
|
|
6704
6617
|
}
|
|
6705
6618
|
};
|
|
6706
|
-
|
|
6619
|
+
|
|
6707
6620
|
//#region src/expression/definitions/index.ts
|
|
6708
6621
|
const expressions = {
|
|
6709
6622
|
"==": Equals,
|
|
@@ -6732,6 +6645,7 @@
|
|
|
6732
6645
|
number: Assertion,
|
|
6733
6646
|
"number-format": NumberFormat,
|
|
6734
6647
|
object: Assertion,
|
|
6648
|
+
semiliteral: Semiliteral,
|
|
6735
6649
|
slice: Slice,
|
|
6736
6650
|
step: Step,
|
|
6737
6651
|
string: Assertion,
|
|
@@ -6744,7 +6658,163 @@
|
|
|
6744
6658
|
distance: Distance,
|
|
6745
6659
|
"global-state": GlobalState
|
|
6746
6660
|
};
|
|
6747
|
-
|
|
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
|
+
|
|
6748
6818
|
//#region src/expression/compound_expression.ts
|
|
6749
6819
|
var CompoundExpression = class CompoundExpression {
|
|
6750
6820
|
constructor(name, type, evaluate, args, key) {
|
|
@@ -7312,28 +7382,14 @@
|
|
|
7312
7382
|
return result;
|
|
7313
7383
|
}
|
|
7314
7384
|
function isGlobalPropertyConstant(e, properties) {
|
|
7315
|
-
if (e instanceof CompoundExpression && properties.indexOf(e.name) >= 0) return false;
|
|
7316
|
-
let result = true;
|
|
7317
|
-
e.eachChild((arg) => {
|
|
7318
|
-
if (result && !isGlobalPropertyConstant(arg, properties)) result = false;
|
|
7319
|
-
});
|
|
7320
|
-
return result;
|
|
7321
|
-
}
|
|
7322
|
-
//#endregion
|
|
7323
|
-
//#region src/util/result.ts
|
|
7324
|
-
function success(value) {
|
|
7325
|
-
return {
|
|
7326
|
-
result: "success",
|
|
7327
|
-
value
|
|
7328
|
-
};
|
|
7329
|
-
}
|
|
7330
|
-
function error(value) {
|
|
7331
|
-
return {
|
|
7332
|
-
result: "error",
|
|
7333
|
-
value
|
|
7334
|
-
};
|
|
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;
|
|
7335
7391
|
}
|
|
7336
|
-
|
|
7392
|
+
|
|
7337
7393
|
//#region src/util/properties.ts
|
|
7338
7394
|
function supportsPropertyExpression(spec) {
|
|
7339
7395
|
return spec["property-type"] === "data-driven" || spec["property-type"] === "cross-faded-data-driven";
|
|
@@ -7344,13 +7400,13 @@
|
|
|
7344
7400
|
function supportsInterpolation(spec) {
|
|
7345
7401
|
return !!spec.expression && spec.expression.interpolated;
|
|
7346
7402
|
}
|
|
7347
|
-
|
|
7403
|
+
|
|
7348
7404
|
//#region src/util/extend.ts
|
|
7349
7405
|
function extendBy(output, ...inputs) {
|
|
7350
7406
|
for (const input of inputs) for (const k in input) output[k] = input[k];
|
|
7351
7407
|
return output;
|
|
7352
7408
|
}
|
|
7353
|
-
|
|
7409
|
+
|
|
7354
7410
|
//#region src/util/get_type.ts
|
|
7355
7411
|
function getType(val) {
|
|
7356
7412
|
if (val instanceof Number) return "number";
|
|
@@ -7360,7 +7416,7 @@
|
|
|
7360
7416
|
else if (val === null) return "null";
|
|
7361
7417
|
else return typeof val;
|
|
7362
7418
|
}
|
|
7363
|
-
|
|
7419
|
+
|
|
7364
7420
|
//#region src/function/index.ts
|
|
7365
7421
|
function isFunction(value) {
|
|
7366
7422
|
return typeof value === "object" && value !== null && !Array.isArray(value) && typeOf(value) === ObjectType;
|
|
@@ -7458,21 +7514,21 @@
|
|
|
7458
7514
|
kind: "source",
|
|
7459
7515
|
evaluate(_, feature) {
|
|
7460
7516
|
const value = feature && feature.properties ? feature.properties[parameters.property] : void 0;
|
|
7461
|
-
if (value === void 0) return coalesce
|
|
7517
|
+
if (value === void 0) return coalesce(parameters.default, propertySpec.default);
|
|
7462
7518
|
return innerFun(parameters, propertySpec, value, hashedStops, categoricalKeyType);
|
|
7463
7519
|
}
|
|
7464
7520
|
};
|
|
7465
7521
|
}
|
|
7466
|
-
function coalesce
|
|
7522
|
+
function coalesce(a, b, c) {
|
|
7467
7523
|
if (a !== void 0) return a;
|
|
7468
7524
|
if (b !== void 0) return b;
|
|
7469
7525
|
if (c !== void 0) return c;
|
|
7470
7526
|
}
|
|
7471
7527
|
function evaluateCategoricalFunction(parameters, propertySpec, input, hashedStops, keyType) {
|
|
7472
|
-
return coalesce
|
|
7528
|
+
return coalesce(typeof input === keyType ? hashedStops[input] : void 0, parameters.default, propertySpec.default);
|
|
7473
7529
|
}
|
|
7474
7530
|
function evaluateIntervalFunction(parameters, propertySpec, input) {
|
|
7475
|
-
if (getType(input) !== "number") return coalesce
|
|
7531
|
+
if (getType(input) !== "number") return coalesce(parameters.default, propertySpec.default);
|
|
7476
7532
|
const n = parameters.stops.length;
|
|
7477
7533
|
if (n === 1) return parameters.stops[0][1];
|
|
7478
7534
|
if (input <= parameters.stops[0][0]) return parameters.stops[0][1];
|
|
@@ -7482,13 +7538,13 @@
|
|
|
7482
7538
|
}
|
|
7483
7539
|
function evaluateExponentialFunction(parameters, propertySpec, input) {
|
|
7484
7540
|
const base = parameters.base !== void 0 ? parameters.base : 1;
|
|
7485
|
-
if (getType(input) !== "number") return coalesce
|
|
7541
|
+
if (getType(input) !== "number") return coalesce(parameters.default, propertySpec.default);
|
|
7486
7542
|
const n = parameters.stops.length;
|
|
7487
7543
|
if (n === 1) return parameters.stops[0][1];
|
|
7488
7544
|
if (input <= parameters.stops[0][0]) return parameters.stops[0][1];
|
|
7489
7545
|
if (input >= parameters.stops[n - 1][0]) return parameters.stops[n - 1][1];
|
|
7490
7546
|
const index = findStopLessThanOrEqualTo(parameters.stops.map((stop) => stop[0]), input, "");
|
|
7491
|
-
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]);
|
|
7492
7548
|
const outputLower = parameters.stops[index][1];
|
|
7493
7549
|
const outputUpper = parameters.stops[index + 1][1];
|
|
7494
7550
|
const interp = interpolateFactory[propertySpec.type] || identityFunction;
|
|
@@ -7522,7 +7578,7 @@
|
|
|
7522
7578
|
break;
|
|
7523
7579
|
default: if (getType(input) !== propertySpec.type && (propertySpec.type !== "enum" || !propertySpec.values[input])) input = void 0;
|
|
7524
7580
|
}
|
|
7525
|
-
return coalesce
|
|
7581
|
+
return coalesce(input, parameters.default, propertySpec.default);
|
|
7526
7582
|
}
|
|
7527
7583
|
/**
|
|
7528
7584
|
* Returns a ratio that can be used to interpolate between exponential function
|
|
@@ -7562,14 +7618,28 @@
|
|
|
7562
7618
|
*
|
|
7563
7619
|
* @private
|
|
7564
7620
|
*/
|
|
7565
|
-
function interpolationFactor(input, base, lowerValue, upperValue) {
|
|
7621
|
+
function interpolationFactor$1(input, base, lowerValue, upperValue) {
|
|
7566
7622
|
const difference = upperValue - lowerValue;
|
|
7567
7623
|
const progress = input - lowerValue;
|
|
7568
7624
|
if (difference === 0) return 0;
|
|
7569
7625
|
else if (base === 1) return progress / difference;
|
|
7570
7626
|
else return (Math.pow(base, progress) - 1) / (Math.pow(base, difference) - 1);
|
|
7571
7627
|
}
|
|
7572
|
-
|
|
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
|
+
|
|
7573
7643
|
//#region src/expression/index.ts
|
|
7574
7644
|
var StyleExpression = class {
|
|
7575
7645
|
constructor(expression, rootKey, propertySpec, globalState) {
|
|
@@ -7851,7 +7921,7 @@
|
|
|
7851
7921
|
globalState
|
|
7852
7922
|
};
|
|
7853
7923
|
}
|
|
7854
|
-
|
|
7924
|
+
|
|
7855
7925
|
//#region src/feature_filter/index.ts
|
|
7856
7926
|
function classifyChildren(children) {
|
|
7857
7927
|
let sawLegacy = false;
|
|
@@ -7957,12 +8027,10 @@
|
|
|
7957
8027
|
if (diagnostic) return diagnostic;
|
|
7958
8028
|
break;
|
|
7959
8029
|
}
|
|
7960
|
-
case "case":
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
}
|
|
7965
|
-
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
|
+
}
|
|
7966
8034
|
}
|
|
7967
8035
|
return null;
|
|
7968
8036
|
}
|
|
@@ -8000,7 +8068,7 @@
|
|
|
8000
8068
|
needGeometry: false,
|
|
8001
8069
|
getGlobalStateRefs: () => /* @__PURE__ */ new Set()
|
|
8002
8070
|
};
|
|
8003
|
-
if (!isExpressionFilter(filter)) filter = convertFilter
|
|
8071
|
+
if (!isExpressionFilter(filter)) filter = convertFilter(filter);
|
|
8004
8072
|
else warnAboutMixedLegacyFilter(filter, rootKey);
|
|
8005
8073
|
const compiled = createExpression(filter, rootKey, filterSpec, globalState);
|
|
8006
8074
|
if (compiled.result === "error") throw new Error(compiled.value.map((err) => `${err.key}: ${err.message}`).join(", "));
|
|
@@ -8019,13 +8087,13 @@
|
|
|
8019
8087
|
for (let index = 1; index < filter.length; index++) if (geometryNeeded(filter[index])) return true;
|
|
8020
8088
|
return false;
|
|
8021
8089
|
}
|
|
8022
|
-
function convertFilter
|
|
8090
|
+
function convertFilter(filter) {
|
|
8023
8091
|
if (!filter) return true;
|
|
8024
8092
|
const op = filter[0];
|
|
8025
8093
|
if (filter.length <= 1) return op !== "any";
|
|
8026
|
-
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;
|
|
8027
8095
|
}
|
|
8028
|
-
function convertComparisonOp
|
|
8096
|
+
function convertComparisonOp(property, value, op) {
|
|
8029
8097
|
switch (property) {
|
|
8030
8098
|
case "$type": return [`filter-type-${op}`, value];
|
|
8031
8099
|
case "$id": return [`filter-id-${op}`, value];
|
|
@@ -8037,9 +8105,9 @@
|
|
|
8037
8105
|
}
|
|
8038
8106
|
}
|
|
8039
8107
|
function convertDisjunctionOp(filters) {
|
|
8040
|
-
return ["any"].concat(filters.map(convertFilter
|
|
8108
|
+
return ["any"].concat(filters.map(convertFilter));
|
|
8041
8109
|
}
|
|
8042
|
-
function convertInOp
|
|
8110
|
+
function convertInOp(property, values) {
|
|
8043
8111
|
if (values.length === 0) return false;
|
|
8044
8112
|
switch (property) {
|
|
8045
8113
|
case "$type": return ["filter-type-in", ["literal", values]];
|
|
@@ -8056,7 +8124,7 @@
|
|
|
8056
8124
|
];
|
|
8057
8125
|
}
|
|
8058
8126
|
}
|
|
8059
|
-
function convertHasOp
|
|
8127
|
+
function convertHasOp(property) {
|
|
8060
8128
|
switch (property) {
|
|
8061
8129
|
case "$type": return true;
|
|
8062
8130
|
case "$id": return ["filter-has-id"];
|
|
@@ -8372,21 +8440,29 @@
|
|
|
8372
8440
|
width;
|
|
8373
8441
|
height;
|
|
8374
8442
|
#svg;
|
|
8375
|
-
#backgroundColor;
|
|
8376
8443
|
#spriteSheetDefs = new Map();
|
|
8377
8444
|
#spriteSymbolDefs = new Map();
|
|
8378
8445
|
#sdfFilterDefs = new Map();
|
|
8379
8446
|
#blurFilterDefs = new Map();
|
|
8447
|
+
#rasterDefs = [];
|
|
8448
|
+
#rasterClipCount = 0;
|
|
8449
|
+
#clipCircle;
|
|
8380
8450
|
constructor(opt) {
|
|
8381
8451
|
this.width = opt.width;
|
|
8382
8452
|
this.height = opt.height;
|
|
8383
8453
|
this.#svg = [];
|
|
8384
|
-
|
|
8454
|
+
}
|
|
8455
|
+
setClipCircle(circle) {
|
|
8456
|
+
this.#clipCircle = circle;
|
|
8385
8457
|
}
|
|
8386
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.)
|
|
8387
8461
|
const color = new Color(style.color);
|
|
8388
8462
|
color.alpha *= style.opacity;
|
|
8389
|
-
|
|
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)} />`);
|
|
8390
8466
|
}
|
|
8391
8467
|
drawPolygons(id, features) {
|
|
8392
8468
|
if (features.length === 0)
|
|
@@ -8394,9 +8470,11 @@
|
|
|
8394
8470
|
// Merge only *consecutive* features with identical attributes, so the paint
|
|
8395
8471
|
// order of overlapping features matches MapLibre's source order. A global
|
|
8396
8472
|
// merge would reorder interleaved features (e.g. data-driven fill colors).
|
|
8397
|
-
//
|
|
8398
|
-
//
|
|
8399
|
-
//
|
|
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.
|
|
8400
8478
|
const fillGroups = [];
|
|
8401
8479
|
const outlineGroups = [];
|
|
8402
8480
|
let currentFillKey;
|
|
@@ -8411,37 +8489,48 @@
|
|
|
8411
8489
|
// Round each ring once; shared by the fill and the outline pass.
|
|
8412
8490
|
const rings = feature.geometry.map((ring) => ring.map((p) => roundXY(p.x, p.y)));
|
|
8413
8491
|
const color = new Color(style.color);
|
|
8492
|
+
const translucent = style.opacity < 1 || color.alpha < 255;
|
|
8414
8493
|
if (color.alpha > 0) {
|
|
8415
8494
|
const key = color.hex + translate + opacityAttr;
|
|
8416
|
-
if (key !== currentFillKey) {
|
|
8495
|
+
if (translucent || key !== currentFillKey) {
|
|
8417
8496
|
fillGroups.push({ segments: [], attrs: `${fillAttr(color)}${translate}${opacityAttr}` });
|
|
8418
|
-
currentFillKey = key;
|
|
8497
|
+
currentFillKey = translucent ? undefined : key;
|
|
8419
8498
|
}
|
|
8420
8499
|
const group = fillGroups[fillGroups.length - 1];
|
|
8421
8500
|
for (const ring of rings)
|
|
8422
8501
|
group.segments.push(ring);
|
|
8423
8502
|
}
|
|
8424
|
-
// fill-antialias outline in
|
|
8425
|
-
//
|
|
8426
|
-
//
|
|
8427
|
-
//
|
|
8428
|
-
//
|
|
8429
|
-
//
|
|
8430
|
-
//
|
|
8431
|
-
if (style.antialias && style.outlineColor !== undefined) {
|
|
8432
|
-
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;
|
|
8433
8512
|
if (outlineColor.alpha > 0) {
|
|
8513
|
+
const outlineTranslucent = translucent || outlineColor.alpha < 255;
|
|
8434
8514
|
const key = outlineColor.hex + translate + opacityAttr;
|
|
8435
|
-
if (key !== currentOutlineKey) {
|
|
8515
|
+
if (outlineTranslucent || key !== currentOutlineKey) {
|
|
8436
8516
|
outlineGroups.push({
|
|
8437
|
-
|
|
8517
|
+
closed: [],
|
|
8518
|
+
open: [],
|
|
8438
8519
|
attrs: `fill="none" ${strokeAttr(outlineColor, formatScaled(FILL_OUTLINE_WIDTH_PX))}${translate}${opacityAttr}`,
|
|
8439
8520
|
});
|
|
8440
|
-
currentOutlineKey = key;
|
|
8521
|
+
currentOutlineKey = outlineTranslucent ? undefined : key;
|
|
8441
8522
|
}
|
|
8442
8523
|
const group = outlineGroups[outlineGroups.length - 1];
|
|
8443
|
-
|
|
8444
|
-
|
|
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
|
+
}
|
|
8445
8534
|
}
|
|
8446
8535
|
}
|
|
8447
8536
|
});
|
|
@@ -8449,8 +8538,9 @@
|
|
|
8449
8538
|
for (const { segments, attrs } of fillGroups) {
|
|
8450
8539
|
this.#svg.push(`<path d="${segmentsToPath(segments, true)}" ${attrs} />`);
|
|
8451
8540
|
}
|
|
8452
|
-
for (const {
|
|
8453
|
-
|
|
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} />`);
|
|
8454
8544
|
}
|
|
8455
8545
|
this.#svg.push('</g>');
|
|
8456
8546
|
}
|
|
@@ -8458,7 +8548,12 @@
|
|
|
8458
8548
|
if (features.length === 0)
|
|
8459
8549
|
return;
|
|
8460
8550
|
// Merge only *consecutive* same-attribute features (see drawPolygons) so the
|
|
8461
|
-
// 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.
|
|
8462
8557
|
const groups = [];
|
|
8463
8558
|
let currentKey;
|
|
8464
8559
|
features.forEach(([feature, style]) => {
|
|
@@ -8493,7 +8588,8 @@
|
|
|
8493
8588
|
translate,
|
|
8494
8589
|
filterAttr,
|
|
8495
8590
|
].join('\0');
|
|
8496
|
-
|
|
8591
|
+
const translucent = effectiveOpacity < 1 || color.alpha < 255;
|
|
8592
|
+
if (translucent || key !== currentKey) {
|
|
8497
8593
|
const attrs = [
|
|
8498
8594
|
'fill="none"',
|
|
8499
8595
|
strokeAttr(color, roundedWidth),
|
|
@@ -8506,8 +8602,9 @@
|
|
|
8506
8602
|
groups.push({
|
|
8507
8603
|
segments: [],
|
|
8508
8604
|
attrs: attrs.join(' ') + translate + opacityAttr + filterAttr,
|
|
8605
|
+
separate: translucent,
|
|
8509
8606
|
});
|
|
8510
|
-
currentKey = key;
|
|
8607
|
+
currentKey = translucent ? undefined : key;
|
|
8511
8608
|
}
|
|
8512
8609
|
const group = groups[groups.length - 1];
|
|
8513
8610
|
feature.geometry.forEach((line) => {
|
|
@@ -8516,7 +8613,13 @@
|
|
|
8516
8613
|
});
|
|
8517
8614
|
});
|
|
8518
8615
|
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
8519
|
-
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
|
+
}
|
|
8520
8623
|
const chains = chainSegments(segments);
|
|
8521
8624
|
const d = segmentsToPath(chains);
|
|
8522
8625
|
this.#svg.push(`<path d="${d}" ${attrs} />`);
|
|
@@ -8773,7 +8876,10 @@
|
|
|
8773
8876
|
gAttrs += ` filter="${filters.join(' ')}"`;
|
|
8774
8877
|
this.#svg.push(`<g ${gAttrs}>`);
|
|
8775
8878
|
const pixelated = style.resampling === 'nearest';
|
|
8879
|
+
this.#drawRasterMeshes(tiles, pixelated);
|
|
8776
8880
|
for (const tile of tiles) {
|
|
8881
|
+
if (tile.triangles)
|
|
8882
|
+
continue;
|
|
8777
8883
|
const overlap = Math.min(tile.width, tile.height) / 10000; // slight overlap to prevent sub-pixel gaps between tiles
|
|
8778
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}"`;
|
|
8779
8885
|
if (pixelated)
|
|
@@ -8782,11 +8888,65 @@
|
|
|
8782
8888
|
}
|
|
8783
8889
|
this.#svg.push('</g>');
|
|
8784
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
|
+
}
|
|
8785
8941
|
getString() {
|
|
8786
8942
|
const w = this.width.toFixed(0);
|
|
8787
8943
|
const h = this.height.toFixed(0);
|
|
8788
8944
|
// Build defs content
|
|
8789
|
-
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];
|
|
8790
8950
|
for (const sheet of this.#spriteSheetDefs.values()) {
|
|
8791
8951
|
defsContent.push(`<image id="${escapeXml(sheet.defId)}" width="${formatNum(sheet.width)}" height="${formatNum(sheet.height)}" xlink:href="${escapeXml(sheet.href)}" />`);
|
|
8792
8952
|
}
|
|
@@ -8813,9 +8973,6 @@
|
|
|
8813
8973
|
`<defs>\n ${defsContent.join('\n ')}\n</defs>`,
|
|
8814
8974
|
`<g id="map" clip-path="url(#vb)">`,
|
|
8815
8975
|
];
|
|
8816
|
-
if (this.#backgroundColor.alpha > 0) {
|
|
8817
|
-
parts.push(`<rect x="-1" y="-1" width="${(this.width + 2).toFixed(0)}" height="${(this.height + 2).toFixed(0)}" ${fillAttr(this.#backgroundColor)} />`);
|
|
8818
|
-
}
|
|
8819
8976
|
parts.push(...this.#svg, '</g>', '</svg>');
|
|
8820
8977
|
return parts.join('\n');
|
|
8821
8978
|
}
|
|
@@ -8835,6 +8992,85 @@
|
|
|
8835
8992
|
function formatScaled(v) {
|
|
8836
8993
|
return formatNum(Math.round(v * 10));
|
|
8837
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
|
+
}
|
|
8838
9074
|
function formatScale(v) {
|
|
8839
9075
|
return (Math.round(v * 10000) / 10000).toString();
|
|
8840
9076
|
}
|
|
@@ -8928,6 +9164,12 @@
|
|
|
8928
9164
|
properties;
|
|
8929
9165
|
patterns;
|
|
8930
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;
|
|
8931
9173
|
#bbox;
|
|
8932
9174
|
constructor(opt) {
|
|
8933
9175
|
this.type = opt.type;
|
|
@@ -8935,6 +9177,7 @@
|
|
|
8935
9177
|
this.properties = opt.properties;
|
|
8936
9178
|
this.patterns = opt.patterns;
|
|
8937
9179
|
this.geometry = opt.geometry;
|
|
9180
|
+
this.outline = opt.outline;
|
|
8938
9181
|
}
|
|
8939
9182
|
getBbox() {
|
|
8940
9183
|
if (this.#bbox)
|
|
@@ -10029,6 +10272,138 @@
|
|
|
10029
10272
|
return str;
|
|
10030
10273
|
}
|
|
10031
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
|
+
|
|
10032
10407
|
const TILE_EXTENT = 4096;
|
|
10033
10408
|
const VTFeatureType = { Unknown: 0, Point: 1, LineString: 2, Polygon: 3 };
|
|
10034
10409
|
async function loadVectorSource(source, job, layerFeatures) {
|
|
@@ -10036,11 +10411,8 @@
|
|
|
10036
10411
|
if (!tiles)
|
|
10037
10412
|
return;
|
|
10038
10413
|
const { width, height } = job.renderer;
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
await Promise.all(tileCoordinates.map(async ({ x, y, offsetX, offsetY }) => {
|
|
10042
|
-
const offset = new Point2D(offsetX, offsetY);
|
|
10043
|
-
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);
|
|
10044
10416
|
if (!tile)
|
|
10045
10417
|
return;
|
|
10046
10418
|
const vectorTile = new VectorTile(new PbfReader(tile.buffer));
|
|
@@ -10052,9 +10424,6 @@
|
|
|
10052
10424
|
}
|
|
10053
10425
|
for (let i = 0; i < layer.length; i++) {
|
|
10054
10426
|
const featureSrc = layer.feature(i);
|
|
10055
|
-
const geometry = featureSrc
|
|
10056
|
-
.loadGeometry()
|
|
10057
|
-
.map((ring) => ring.map((point) => new Point2D(point.x, point.y).scale(tileSize / TILE_EXTENT).translate(offset)));
|
|
10058
10427
|
let type;
|
|
10059
10428
|
let list;
|
|
10060
10429
|
switch (featureSrc.type) {
|
|
@@ -10073,6 +10442,22 @@
|
|
|
10073
10442
|
list = features.polygons;
|
|
10074
10443
|
break;
|
|
10075
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;
|
|
10076
10461
|
// Split MultiPoint into individual Point features
|
|
10077
10462
|
if (type === 'Point' && geometry.length > 1) {
|
|
10078
10463
|
for (const ring of geometry) {
|
|
@@ -10090,6 +10475,7 @@
|
|
|
10090
10475
|
const feature = new Feature({
|
|
10091
10476
|
type,
|
|
10092
10477
|
geometry,
|
|
10478
|
+
outline,
|
|
10093
10479
|
id: featureSrc.id,
|
|
10094
10480
|
properties: featureSrc.properties,
|
|
10095
10481
|
});
|
|
@@ -10100,9 +10486,41 @@
|
|
|
10100
10486
|
}
|
|
10101
10487
|
}));
|
|
10102
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
|
+
}
|
|
10103
10521
|
|
|
10104
10522
|
function loadGeoJSONSource(options) {
|
|
10105
|
-
const { sourceName, data, width, height, zoom, center, layerFeatures } = options;
|
|
10523
|
+
const { sourceName, data, width, height, zoom, center, layerFeatures, projection } = options;
|
|
10106
10524
|
const existing = layerFeatures.get(sourceName);
|
|
10107
10525
|
const features = existing ?? {
|
|
10108
10526
|
points: [],
|
|
@@ -10115,9 +10533,17 @@
|
|
|
10115
10533
|
layerFeatures.set(sourceName, features);
|
|
10116
10534
|
const worldSize = 512 * 2 ** zoom;
|
|
10117
10535
|
const centerMercator = new Point2D(center[0], center[1]).getProject2Pixel();
|
|
10118
|
-
function
|
|
10536
|
+
function toMercator(coord) {
|
|
10119
10537
|
const mercator = new Point2D(coord[0], coord[1]).getProject2Pixel();
|
|
10120
|
-
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)));
|
|
10121
10547
|
}
|
|
10122
10548
|
function makeFeature(type, geometry, id, properties) {
|
|
10123
10549
|
const feature = new Feature({ type, geometry, id, properties });
|
|
@@ -10128,7 +10554,10 @@
|
|
|
10128
10554
|
function extractPoints(geometry) {
|
|
10129
10555
|
return geometry.flatMap((ring) => ring.map((p) => [p]));
|
|
10130
10556
|
}
|
|
10131
|
-
function addFeature(type,
|
|
10557
|
+
function addFeature(type, rings, id, properties) {
|
|
10558
|
+
const geometry = project(type, rings);
|
|
10559
|
+
if (geometry.length === 0)
|
|
10560
|
+
return;
|
|
10132
10561
|
switch (type) {
|
|
10133
10562
|
case 'Point': {
|
|
10134
10563
|
const f = makeFeature('Point', geometry, id, properties);
|
|
@@ -10171,23 +10600,23 @@
|
|
|
10171
10600
|
function processGeometry(geom, id, properties) {
|
|
10172
10601
|
switch (geom.type) {
|
|
10173
10602
|
case 'Point':
|
|
10174
|
-
addFeature('Point', [[
|
|
10603
|
+
addFeature('Point', [[toMercator(geom.coordinates)]], id, properties);
|
|
10175
10604
|
break;
|
|
10176
10605
|
case 'MultiPoint':
|
|
10177
|
-
addFeature('Point', geom.coordinates.map((c) => [
|
|
10606
|
+
addFeature('Point', geom.coordinates.map((c) => [toMercator(c)]), id, properties);
|
|
10178
10607
|
break;
|
|
10179
10608
|
case 'LineString':
|
|
10180
|
-
addFeature('LineString', [geom.coordinates.map((c) =>
|
|
10609
|
+
addFeature('LineString', [geom.coordinates.map((c) => toMercator(c))], id, properties);
|
|
10181
10610
|
break;
|
|
10182
10611
|
case 'MultiLineString':
|
|
10183
|
-
addFeature('LineString', geom.coordinates.map((line) => line.map((c) =>
|
|
10612
|
+
addFeature('LineString', geom.coordinates.map((line) => line.map((c) => toMercator(c))), id, properties);
|
|
10184
10613
|
break;
|
|
10185
10614
|
case 'Polygon':
|
|
10186
|
-
addFeature('Polygon', geom.coordinates.map((ring) => ring.map((c) =>
|
|
10615
|
+
addFeature('Polygon', geom.coordinates.map((ring) => ring.map((c) => toMercator(c))), id, properties);
|
|
10187
10616
|
break;
|
|
10188
10617
|
case 'MultiPolygon':
|
|
10189
10618
|
for (const polygon of geom.coordinates) {
|
|
10190
|
-
addFeature('Polygon', polygon.map((ring) => ring.map((c) =>
|
|
10619
|
+
addFeature('Polygon', polygon.map((ring) => ring.map((c) => toMercator(c))), id, properties);
|
|
10191
10620
|
}
|
|
10192
10621
|
break;
|
|
10193
10622
|
case 'GeometryCollection':
|
|
@@ -10211,6 +10640,22 @@
|
|
|
10211
10640
|
break;
|
|
10212
10641
|
}
|
|
10213
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
|
+
}
|
|
10214
10659
|
|
|
10215
10660
|
/**
|
|
10216
10661
|
* Base64-encode an ArrayBuffer (standard alphabet, with padding).
|
|
@@ -10248,6 +10693,23 @@
|
|
|
10248
10693
|
throw Error(`Invalid raster source "${sourceName}": expected type "raster" with a "tiles" array`);
|
|
10249
10694
|
}
|
|
10250
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
|
+
}
|
|
10251
10713
|
const { zoomLevel, tileSize, tiles } = calculateTileGrid(width, height, center, zoom, source.maxzoom);
|
|
10252
10714
|
const rasterTiles = await Promise.all(tiles.map(async ({ x, y, offsetX, offsetY }) => {
|
|
10253
10715
|
const tile = await getTile(sourceUrl, zoomLevel, x, y);
|
|
@@ -10351,6 +10813,7 @@
|
|
|
10351
10813
|
zoom,
|
|
10352
10814
|
center,
|
|
10353
10815
|
layerFeatures,
|
|
10816
|
+
projection: job.projection,
|
|
10354
10817
|
});
|
|
10355
10818
|
}
|
|
10356
10819
|
break;
|
|
@@ -10482,6 +10945,594 @@
|
|
|
10482
10945
|
return layers.map(createStyleLayer);
|
|
10483
10946
|
}
|
|
10484
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
|
+
|
|
10485
11536
|
function resolveTokens(text, properties) {
|
|
10486
11537
|
return text.replace(/\{([^}]+)\}/g, (_, key) => {
|
|
10487
11538
|
const value = properties[key];
|
|
@@ -10495,6 +11546,16 @@
|
|
|
10495
11546
|
});
|
|
10496
11547
|
}
|
|
10497
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);
|
|
10498
11559
|
await render(job);
|
|
10499
11560
|
return job.renderer.getString();
|
|
10500
11561
|
}
|