ngx-open-map-wrapper/node_modules/@maplibre/maplibre-gl-style-spec/dist/index.mjs

11632 lines
342 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

var $version = 8;
var $root = {
version: {
required: true,
type: "enum",
values: [
8
]
},
name: {
type: "string"
},
metadata: {
type: "*"
},
center: {
type: "array",
value: "number"
},
centerAltitude: {
type: "number"
},
zoom: {
type: "number"
},
bearing: {
type: "number",
"default": 0,
period: 360,
units: "degrees"
},
pitch: {
type: "number",
"default": 0,
units: "degrees"
},
roll: {
type: "number",
"default": 0,
units: "degrees"
},
state: {
type: "state",
"default": {
}
},
light: {
type: "light"
},
sky: {
type: "sky"
},
projection: {
type: "projection"
},
terrain: {
type: "terrain"
},
sources: {
required: true,
type: "sources"
},
sprite: {
type: "sprite"
},
glyphs: {
type: "string"
},
"font-faces": {
type: "array",
value: "fontFaces"
},
transition: {
type: "transition"
},
layers: {
required: true,
type: "array",
value: "layer"
}
};
var sources = {
"*": {
type: "source"
}
};
var source = [
"source_vector",
"source_raster",
"source_raster_dem",
"source_geojson",
"source_video",
"source_image"
];
var source_vector = {
type: {
required: true,
type: "enum",
values: {
vector: {
}
}
},
url: {
type: "string"
},
tiles: {
type: "array",
value: "string"
},
bounds: {
type: "array",
value: "number",
length: 4,
"default": [
-180,
-85.051129,
180,
85.051129
]
},
scheme: {
type: "enum",
values: {
xyz: {
},
tms: {
}
},
"default": "xyz"
},
minzoom: {
type: "number",
"default": 0
},
maxzoom: {
type: "number",
"default": 22
},
attribution: {
type: "string"
},
promoteId: {
type: "promoteId"
},
volatile: {
type: "boolean",
"default": false
},
"*": {
type: "*"
}
};
var source_raster = {
type: {
required: true,
type: "enum",
values: {
raster: {
}
}
},
url: {
type: "string"
},
tiles: {
type: "array",
value: "string"
},
bounds: {
type: "array",
value: "number",
length: 4,
"default": [
-180,
-85.051129,
180,
85.051129
]
},
minzoom: {
type: "number",
"default": 0
},
maxzoom: {
type: "number",
"default": 22
},
tileSize: {
type: "number",
"default": 512,
units: "pixels"
},
scheme: {
type: "enum",
values: {
xyz: {
},
tms: {
}
},
"default": "xyz"
},
attribution: {
type: "string"
},
volatile: {
type: "boolean",
"default": false
},
"*": {
type: "*"
}
};
var source_raster_dem = {
type: {
required: true,
type: "enum",
values: {
"raster-dem": {
}
}
},
url: {
type: "string"
},
tiles: {
type: "array",
value: "string"
},
bounds: {
type: "array",
value: "number",
length: 4,
"default": [
-180,
-85.051129,
180,
85.051129
]
},
minzoom: {
type: "number",
"default": 0
},
maxzoom: {
type: "number",
"default": 22
},
tileSize: {
type: "number",
"default": 512,
units: "pixels"
},
attribution: {
type: "string"
},
encoding: {
type: "enum",
values: {
terrarium: {
},
mapbox: {
},
custom: {
}
},
"default": "mapbox"
},
redFactor: {
type: "number",
"default": 1
},
blueFactor: {
type: "number",
"default": 1
},
greenFactor: {
type: "number",
"default": 1
},
baseShift: {
type: "number",
"default": 0
},
volatile: {
type: "boolean",
"default": false
},
"*": {
type: "*"
}
};
var source_geojson = {
type: {
required: true,
type: "enum",
values: {
geojson: {
}
}
},
data: {
required: true,
type: "*"
},
maxzoom: {
type: "number",
"default": 18
},
attribution: {
type: "string"
},
buffer: {
type: "number",
"default": 128,
maximum: 512,
minimum: 0
},
filter: {
type: "*"
},
tolerance: {
type: "number",
"default": 0.375
},
cluster: {
type: "boolean",
"default": false
},
clusterRadius: {
type: "number",
"default": 50,
minimum: 0
},
clusterMaxZoom: {
type: "number"
},
clusterMinPoints: {
type: "number"
},
clusterProperties: {
type: "*"
},
lineMetrics: {
type: "boolean",
"default": false
},
generateId: {
type: "boolean",
"default": false
},
promoteId: {
type: "promoteId"
}
};
var source_video = {
type: {
required: true,
type: "enum",
values: {
video: {
}
}
},
urls: {
required: true,
type: "array",
value: "string"
},
coordinates: {
required: true,
type: "array",
length: 4,
value: {
type: "array",
length: 2,
value: "number"
}
}
};
var source_image = {
type: {
required: true,
type: "enum",
values: {
image: {
}
}
},
url: {
required: true,
type: "string"
},
coordinates: {
required: true,
type: "array",
length: 4,
value: {
type: "array",
length: 2,
value: "number"
}
}
};
var layer = {
id: {
type: "string",
required: true
},
type: {
type: "enum",
values: {
fill: {
},
line: {
},
symbol: {
},
circle: {
},
heatmap: {
},
"fill-extrusion": {
},
raster: {
},
hillshade: {
},
"color-relief": {
},
background: {
}
},
required: true
},
metadata: {
type: "*"
},
source: {
type: "string"
},
"source-layer": {
type: "string"
},
minzoom: {
type: "number",
minimum: 0,
maximum: 24
},
maxzoom: {
type: "number",
minimum: 0,
maximum: 24
},
filter: {
type: "filter"
},
layout: {
type: "layout"
},
paint: {
type: "paint"
}
};
var layout = [
"layout_fill",
"layout_line",
"layout_circle",
"layout_heatmap",
"layout_fill-extrusion",
"layout_symbol",
"layout_raster",
"layout_hillshade",
"layout_color-relief",
"layout_background"
];
var layout_background = {
visibility: {
type: "enum",
values: {
visible: {
},
none: {
}
},
"default": "visible",
"property-type": "constant"
}
};
var layout_fill = {
"fill-sort-key": {
type: "number",
expression: {
interpolated: false,
parameters: [
"zoom",
"feature"
]
},
"property-type": "data-driven"
},
visibility: {
type: "enum",
values: {
visible: {
},
none: {
}
},
"default": "visible",
"property-type": "constant"
}
};
var layout_circle = {
"circle-sort-key": {
type: "number",
expression: {
interpolated: false,
parameters: [
"zoom",
"feature"
]
},
"property-type": "data-driven"
},
visibility: {
type: "enum",
values: {
visible: {
},
none: {
}
},
"default": "visible",
"property-type": "constant"
}
};
var layout_heatmap = {
visibility: {
type: "enum",
values: {
visible: {
},
none: {
}
},
"default": "visible",
"property-type": "constant"
}
};
var layout_line = {
"line-cap": {
type: "enum",
values: {
butt: {
},
round: {
},
square: {
}
},
"default": "butt",
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"line-join": {
type: "enum",
values: {
bevel: {
},
round: {
},
miter: {
}
},
"default": "miter",
expression: {
interpolated: false,
parameters: [
"zoom",
"feature"
]
},
"property-type": "data-driven"
},
"line-miter-limit": {
type: "number",
"default": 2,
requires: [
{
"line-join": "miter"
}
],
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"line-round-limit": {
type: "number",
"default": 1.05,
requires: [
{
"line-join": "round"
}
],
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"line-sort-key": {
type: "number",
expression: {
interpolated: false,
parameters: [
"zoom",
"feature"
]
},
"property-type": "data-driven"
},
visibility: {
type: "enum",
values: {
visible: {
},
none: {
}
},
"default": "visible",
"property-type": "constant"
}
};
var layout_symbol = {
"symbol-placement": {
type: "enum",
values: {
point: {
},
line: {
},
"line-center": {
}
},
"default": "point",
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"symbol-spacing": {
type: "number",
"default": 250,
minimum: 1,
units: "pixels",
requires: [
{
"symbol-placement": "line"
}
],
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"symbol-avoid-edges": {
type: "boolean",
"default": false,
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"symbol-sort-key": {
type: "number",
expression: {
interpolated: false,
parameters: [
"zoom",
"feature"
]
},
"property-type": "data-driven"
},
"symbol-z-order": {
type: "enum",
values: {
auto: {
},
"viewport-y": {
},
source: {
}
},
"default": "auto",
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"icon-allow-overlap": {
type: "boolean",
"default": false,
requires: [
"icon-image",
{
"!": "icon-overlap"
}
],
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"icon-overlap": {
type: "enum",
values: {
never: {
},
always: {
},
cooperative: {
}
},
requires: [
"icon-image"
],
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"icon-ignore-placement": {
type: "boolean",
"default": false,
requires: [
"icon-image"
],
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"icon-optional": {
type: "boolean",
"default": false,
requires: [
"icon-image",
"text-field"
],
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"icon-rotation-alignment": {
type: "enum",
values: {
map: {
},
viewport: {
},
auto: {
}
},
"default": "auto",
requires: [
"icon-image"
],
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"icon-size": {
type: "number",
"default": 1,
minimum: 0,
units: "factor of the original icon size",
requires: [
"icon-image"
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature"
]
},
"property-type": "data-driven"
},
"icon-text-fit": {
type: "enum",
values: {
none: {
},
width: {
},
height: {
},
both: {
}
},
"default": "none",
requires: [
"icon-image",
"text-field"
],
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"icon-text-fit-padding": {
type: "array",
value: "number",
length: 4,
"default": [
0,
0,
0,
0
],
units: "pixels",
requires: [
"icon-image",
"text-field",
{
"icon-text-fit": [
"both",
"width",
"height"
]
}
],
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"icon-image": {
type: "resolvedImage",
tokens: true,
expression: {
interpolated: false,
parameters: [
"zoom",
"feature"
]
},
"property-type": "data-driven"
},
"icon-rotate": {
type: "number",
"default": 0,
period: 360,
units: "degrees",
requires: [
"icon-image"
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature"
]
},
"property-type": "data-driven"
},
"icon-padding": {
type: "padding",
"default": [
2
],
units: "pixels",
requires: [
"icon-image"
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature"
]
},
"property-type": "data-driven"
},
"icon-keep-upright": {
type: "boolean",
"default": false,
requires: [
"icon-image",
{
"icon-rotation-alignment": "map"
},
{
"symbol-placement": [
"line",
"line-center"
]
}
],
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"icon-offset": {
type: "array",
value: "number",
length: 2,
"default": [
0,
0
],
requires: [
"icon-image"
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature"
]
},
"property-type": "data-driven"
},
"icon-anchor": {
type: "enum",
values: {
center: {
},
left: {
},
right: {
},
top: {
},
bottom: {
},
"top-left": {
},
"top-right": {
},
"bottom-left": {
},
"bottom-right": {
}
},
"default": "center",
requires: [
"icon-image"
],
expression: {
interpolated: false,
parameters: [
"zoom",
"feature"
]
},
"property-type": "data-driven"
},
"icon-pitch-alignment": {
type: "enum",
values: {
map: {
},
viewport: {
},
auto: {
}
},
"default": "auto",
requires: [
"icon-image"
],
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"text-pitch-alignment": {
type: "enum",
values: {
map: {
},
viewport: {
},
auto: {
}
},
"default": "auto",
requires: [
"text-field"
],
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"text-rotation-alignment": {
type: "enum",
values: {
map: {
},
viewport: {
},
"viewport-glyph": {
},
auto: {
}
},
"default": "auto",
requires: [
"text-field"
],
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"text-field": {
type: "formatted",
"default": "",
tokens: true,
expression: {
interpolated: false,
parameters: [
"zoom",
"feature"
]
},
"property-type": "data-driven"
},
"text-font": {
type: "array",
value: "string",
"default": [
"Open Sans Regular",
"Arial Unicode MS Regular"
],
requires: [
"text-field"
],
expression: {
interpolated: false,
parameters: [
"zoom",
"feature"
]
},
"property-type": "data-driven"
},
"text-size": {
type: "number",
"default": 16,
minimum: 0,
units: "pixels",
requires: [
"text-field"
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature"
]
},
"property-type": "data-driven"
},
"text-max-width": {
type: "number",
"default": 10,
minimum: 0,
units: "ems",
requires: [
"text-field"
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature"
]
},
"property-type": "data-driven"
},
"text-line-height": {
type: "number",
"default": 1.2,
units: "ems",
requires: [
"text-field"
],
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"text-letter-spacing": {
type: "number",
"default": 0,
units: "ems",
requires: [
"text-field"
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature"
]
},
"property-type": "data-driven"
},
"text-justify": {
type: "enum",
values: {
auto: {
},
left: {
},
center: {
},
right: {
}
},
"default": "center",
requires: [
"text-field"
],
expression: {
interpolated: false,
parameters: [
"zoom",
"feature"
]
},
"property-type": "data-driven"
},
"text-radial-offset": {
type: "number",
units: "ems",
"default": 0,
requires: [
"text-field"
],
"property-type": "data-driven",
expression: {
interpolated: true,
parameters: [
"zoom",
"feature"
]
}
},
"text-variable-anchor": {
type: "array",
value: "enum",
values: {
center: {
},
left: {
},
right: {
},
top: {
},
bottom: {
},
"top-left": {
},
"top-right": {
},
"bottom-left": {
},
"bottom-right": {
}
},
requires: [
"text-field",
{
"symbol-placement": [
"point"
]
}
],
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"text-variable-anchor-offset": {
type: "variableAnchorOffsetCollection",
requires: [
"text-field",
{
"symbol-placement": [
"point"
]
}
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature"
]
},
"property-type": "data-driven"
},
"text-anchor": {
type: "enum",
values: {
center: {
},
left: {
},
right: {
},
top: {
},
bottom: {
},
"top-left": {
},
"top-right": {
},
"bottom-left": {
},
"bottom-right": {
}
},
"default": "center",
requires: [
"text-field",
{
"!": "text-variable-anchor"
}
],
expression: {
interpolated: false,
parameters: [
"zoom",
"feature"
]
},
"property-type": "data-driven"
},
"text-max-angle": {
type: "number",
"default": 45,
units: "degrees",
requires: [
"text-field",
{
"symbol-placement": [
"line",
"line-center"
]
}
],
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"text-writing-mode": {
type: "array",
value: "enum",
values: {
horizontal: {
},
vertical: {
}
},
requires: [
"text-field",
{
"symbol-placement": [
"point"
]
}
],
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"text-rotate": {
type: "number",
"default": 0,
period: 360,
units: "degrees",
requires: [
"text-field"
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature"
]
},
"property-type": "data-driven"
},
"text-padding": {
type: "number",
"default": 2,
minimum: 0,
units: "pixels",
requires: [
"text-field"
],
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"text-keep-upright": {
type: "boolean",
"default": true,
requires: [
"text-field",
{
"text-rotation-alignment": "map"
},
{
"symbol-placement": [
"line",
"line-center"
]
}
],
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"text-transform": {
type: "enum",
values: {
none: {
},
uppercase: {
},
lowercase: {
}
},
"default": "none",
requires: [
"text-field"
],
expression: {
interpolated: false,
parameters: [
"zoom",
"feature"
]
},
"property-type": "data-driven"
},
"text-offset": {
type: "array",
value: "number",
units: "ems",
length: 2,
"default": [
0,
0
],
requires: [
"text-field",
{
"!": "text-radial-offset"
}
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature"
]
},
"property-type": "data-driven"
},
"text-allow-overlap": {
type: "boolean",
"default": false,
requires: [
"text-field",
{
"!": "text-overlap"
}
],
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"text-overlap": {
type: "enum",
values: {
never: {
},
always: {
},
cooperative: {
}
},
requires: [
"text-field"
],
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"text-ignore-placement": {
type: "boolean",
"default": false,
requires: [
"text-field"
],
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"text-optional": {
type: "boolean",
"default": false,
requires: [
"text-field",
"icon-image"
],
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
visibility: {
type: "enum",
values: {
visible: {
},
none: {
}
},
"default": "visible",
"property-type": "constant"
}
};
var layout_raster = {
visibility: {
type: "enum",
values: {
visible: {
},
none: {
}
},
"default": "visible",
"property-type": "constant"
}
};
var layout_hillshade = {
visibility: {
type: "enum",
values: {
visible: {
},
none: {
}
},
"default": "visible",
"property-type": "constant"
}
};
var filter = {
type: "array",
value: "*"
};
var filter_operator = {
type: "enum",
values: {
"==": {
},
"!=": {
},
">": {
},
">=": {
},
"<": {
},
"<=": {
},
"in": {
},
"!in": {
},
all: {
},
any: {
},
none: {
},
has: {
},
"!has": {
}
}
};
var geometry_type = {
type: "enum",
values: {
Point: {
},
LineString: {
},
Polygon: {
}
}
};
var function_stop = {
type: "array",
minimum: 0,
maximum: 24,
value: [
"number",
"color"
],
length: 2
};
var expression$1 = {
type: "array",
value: "*",
minimum: 1
};
var light = {
anchor: {
type: "enum",
"default": "viewport",
values: {
map: {
},
viewport: {
}
},
"property-type": "data-constant",
transition: false,
expression: {
interpolated: false,
parameters: [
"zoom"
]
}
},
position: {
type: "array",
"default": [
1.15,
210,
30
],
length: 3,
value: "number",
"property-type": "data-constant",
transition: true,
expression: {
interpolated: true,
parameters: [
"zoom"
]
}
},
color: {
type: "color",
"property-type": "data-constant",
"default": "#ffffff",
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
transition: true
},
intensity: {
type: "number",
"property-type": "data-constant",
"default": 0.5,
minimum: 0,
maximum: 1,
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
transition: true
}
};
var sky = {
"sky-color": {
type: "color",
"property-type": "data-constant",
"default": "#88C6FC",
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
transition: true
},
"horizon-color": {
type: "color",
"property-type": "data-constant",
"default": "#ffffff",
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
transition: true
},
"fog-color": {
type: "color",
"property-type": "data-constant",
"default": "#ffffff",
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
transition: true
},
"fog-ground-blend": {
type: "number",
"property-type": "data-constant",
"default": 0.5,
minimum: 0,
maximum: 1,
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
transition: true
},
"horizon-fog-blend": {
type: "number",
"property-type": "data-constant",
"default": 0.8,
minimum: 0,
maximum: 1,
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
transition: true
},
"sky-horizon-blend": {
type: "number",
"property-type": "data-constant",
"default": 0.8,
minimum: 0,
maximum: 1,
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
transition: true
},
"atmosphere-blend": {
type: "number",
"property-type": "data-constant",
"default": 0.8,
minimum: 0,
maximum: 1,
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
transition: true
}
};
var terrain = {
source: {
type: "string",
required: true
},
exaggeration: {
type: "number",
minimum: 0,
"default": 1
}
};
var projection = {
type: {
type: "projectionDefinition",
"default": "mercator",
"property-type": "data-constant",
transition: false,
expression: {
interpolated: true,
parameters: [
"zoom"
]
}
}
};
var paint = [
"paint_fill",
"paint_line",
"paint_circle",
"paint_heatmap",
"paint_fill-extrusion",
"paint_symbol",
"paint_raster",
"paint_hillshade",
"paint_color-relief",
"paint_background"
];
var paint_fill = {
"fill-antialias": {
type: "boolean",
"default": true,
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"fill-opacity": {
type: "number",
"default": 1,
minimum: 0,
maximum: 1,
transition: true,
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"fill-color": {
type: "color",
"default": "#000000",
transition: true,
requires: [
{
"!": "fill-pattern"
}
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"fill-outline-color": {
type: "color",
transition: true,
requires: [
{
"!": "fill-pattern"
},
{
"fill-antialias": true
}
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"fill-translate": {
type: "array",
value: "number",
length: 2,
"default": [
0,
0
],
transition: true,
units: "pixels",
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"fill-translate-anchor": {
type: "enum",
values: {
map: {
},
viewport: {
}
},
"default": "map",
requires: [
"fill-translate"
],
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"fill-pattern": {
type: "resolvedImage",
transition: true,
expression: {
interpolated: false,
parameters: [
"zoom",
"feature"
]
},
"property-type": "cross-faded-data-driven"
}
};
var paint_line = {
"line-opacity": {
type: "number",
"default": 1,
minimum: 0,
maximum: 1,
transition: true,
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"line-color": {
type: "color",
"default": "#000000",
transition: true,
requires: [
{
"!": "line-pattern"
}
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"line-translate": {
type: "array",
value: "number",
length: 2,
"default": [
0,
0
],
transition: true,
units: "pixels",
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"line-translate-anchor": {
type: "enum",
values: {
map: {
},
viewport: {
}
},
"default": "map",
requires: [
"line-translate"
],
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"line-width": {
type: "number",
"default": 1,
minimum: 0,
transition: true,
units: "pixels",
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"line-gap-width": {
type: "number",
"default": 0,
minimum: 0,
transition: true,
units: "pixels",
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"line-offset": {
type: "number",
"default": 0,
transition: true,
units: "pixels",
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"line-blur": {
type: "number",
"default": 0,
minimum: 0,
transition: true,
units: "pixels",
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"line-dasharray": {
type: "array",
value: "number",
minimum: 0,
transition: true,
units: "line widths",
requires: [
{
"!": "line-pattern"
}
],
expression: {
interpolated: false,
parameters: [
"zoom",
"feature"
]
},
"property-type": "cross-faded-data-driven"
},
"line-pattern": {
type: "resolvedImage",
transition: true,
expression: {
interpolated: false,
parameters: [
"zoom",
"feature"
]
},
"property-type": "cross-faded-data-driven"
},
"line-gradient": {
type: "color",
transition: false,
requires: [
{
"!": "line-dasharray"
},
{
"!": "line-pattern"
},
{
source: "geojson",
has: {
lineMetrics: true
}
}
],
expression: {
interpolated: true,
parameters: [
"line-progress"
]
},
"property-type": "color-ramp"
}
};
var paint_circle = {
"circle-radius": {
type: "number",
"default": 5,
minimum: 0,
transition: true,
units: "pixels",
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"circle-color": {
type: "color",
"default": "#000000",
transition: true,
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"circle-blur": {
type: "number",
"default": 0,
transition: true,
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"circle-opacity": {
type: "number",
"default": 1,
minimum: 0,
maximum: 1,
transition: true,
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"circle-translate": {
type: "array",
value: "number",
length: 2,
"default": [
0,
0
],
transition: true,
units: "pixels",
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"circle-translate-anchor": {
type: "enum",
values: {
map: {
},
viewport: {
}
},
"default": "map",
requires: [
"circle-translate"
],
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"circle-pitch-scale": {
type: "enum",
values: {
map: {
},
viewport: {
}
},
"default": "map",
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"circle-pitch-alignment": {
type: "enum",
values: {
map: {
},
viewport: {
}
},
"default": "viewport",
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"circle-stroke-width": {
type: "number",
"default": 0,
minimum: 0,
transition: true,
units: "pixels",
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"circle-stroke-color": {
type: "color",
"default": "#000000",
transition: true,
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"circle-stroke-opacity": {
type: "number",
"default": 1,
minimum: 0,
maximum: 1,
transition: true,
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
}
};
var paint_heatmap = {
"heatmap-radius": {
type: "number",
"default": 30,
minimum: 1,
transition: true,
units: "pixels",
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"heatmap-weight": {
type: "number",
"default": 1,
minimum: 0,
transition: false,
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"heatmap-intensity": {
type: "number",
"default": 1,
minimum: 0,
transition: true,
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"heatmap-color": {
type: "color",
"default": [
"interpolate",
[
"linear"
],
[
"heatmap-density"
],
0,
"rgba(0, 0, 255, 0)",
0.1,
"royalblue",
0.3,
"cyan",
0.5,
"lime",
0.7,
"yellow",
1,
"red"
],
transition: false,
expression: {
interpolated: true,
parameters: [
"heatmap-density"
]
},
"property-type": "color-ramp"
},
"heatmap-opacity": {
type: "number",
"default": 1,
minimum: 0,
maximum: 1,
transition: true,
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
}
};
var paint_symbol = {
"icon-opacity": {
type: "number",
"default": 1,
minimum: 0,
maximum: 1,
transition: true,
requires: [
"icon-image"
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"icon-color": {
type: "color",
"default": "#000000",
transition: true,
requires: [
"icon-image"
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"icon-halo-color": {
type: "color",
"default": "rgba(0, 0, 0, 0)",
transition: true,
requires: [
"icon-image"
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"icon-halo-width": {
type: "number",
"default": 0,
minimum: 0,
transition: true,
units: "pixels",
requires: [
"icon-image"
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"icon-halo-blur": {
type: "number",
"default": 0,
minimum: 0,
transition: true,
units: "pixels",
requires: [
"icon-image"
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"icon-translate": {
type: "array",
value: "number",
length: 2,
"default": [
0,
0
],
transition: true,
units: "pixels",
requires: [
"icon-image"
],
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"icon-translate-anchor": {
type: "enum",
values: {
map: {
},
viewport: {
}
},
"default": "map",
requires: [
"icon-image",
"icon-translate"
],
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"text-opacity": {
type: "number",
"default": 1,
minimum: 0,
maximum: 1,
transition: true,
requires: [
"text-field"
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"text-color": {
type: "color",
"default": "#000000",
transition: true,
overridable: true,
requires: [
"text-field"
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"text-halo-color": {
type: "color",
"default": "rgba(0, 0, 0, 0)",
transition: true,
requires: [
"text-field"
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"text-halo-width": {
type: "number",
"default": 0,
minimum: 0,
transition: true,
units: "pixels",
requires: [
"text-field"
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"text-halo-blur": {
type: "number",
"default": 0,
minimum: 0,
transition: true,
units: "pixels",
requires: [
"text-field"
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"text-translate": {
type: "array",
value: "number",
length: 2,
"default": [
0,
0
],
transition: true,
units: "pixels",
requires: [
"text-field"
],
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"text-translate-anchor": {
type: "enum",
values: {
map: {
},
viewport: {
}
},
"default": "map",
requires: [
"text-field",
"text-translate"
],
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
}
};
var paint_raster = {
"raster-opacity": {
type: "number",
"default": 1,
minimum: 0,
maximum: 1,
transition: true,
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"raster-hue-rotate": {
type: "number",
"default": 0,
period: 360,
transition: true,
units: "degrees",
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"raster-brightness-min": {
type: "number",
"default": 0,
minimum: 0,
maximum: 1,
transition: true,
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"raster-brightness-max": {
type: "number",
"default": 1,
minimum: 0,
maximum: 1,
transition: true,
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"raster-saturation": {
type: "number",
"default": 0,
minimum: -1,
maximum: 1,
transition: true,
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"raster-contrast": {
type: "number",
"default": 0,
minimum: -1,
maximum: 1,
transition: true,
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"raster-resampling": {
type: "enum",
values: {
linear: {
},
nearest: {
}
},
"default": "linear",
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"raster-fade-duration": {
type: "number",
"default": 300,
minimum: 0,
transition: false,
units: "milliseconds",
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
}
};
var paint_hillshade = {
"hillshade-illumination-direction": {
type: "numberArray",
"default": 335,
minimum: 0,
maximum: 359,
transition: false,
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"hillshade-illumination-altitude": {
type: "numberArray",
"default": 45,
minimum: 0,
maximum: 90,
transition: false,
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"hillshade-illumination-anchor": {
type: "enum",
values: {
map: {
},
viewport: {
}
},
"default": "viewport",
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"hillshade-exaggeration": {
type: "number",
"default": 0.5,
minimum: 0,
maximum: 1,
transition: true,
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"hillshade-shadow-color": {
type: "colorArray",
"default": "#000000",
transition: true,
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"hillshade-highlight-color": {
type: "colorArray",
"default": "#FFFFFF",
transition: true,
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"hillshade-accent-color": {
type: "color",
"default": "#000000",
transition: true,
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"hillshade-method": {
type: "enum",
values: {
standard: {
},
basic: {
},
combined: {
},
igor: {
},
multidirectional: {
}
},
"default": "standard",
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
}
};
var paint_background = {
"background-color": {
type: "color",
"default": "#000000",
transition: true,
requires: [
{
"!": "background-pattern"
}
],
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"background-pattern": {
type: "resolvedImage",
transition: true,
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "cross-faded"
},
"background-opacity": {
type: "number",
"default": 1,
minimum: 0,
maximum: 1,
transition: true,
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
}
};
var transition = {
duration: {
type: "number",
"default": 300,
minimum: 0,
units: "milliseconds"
},
delay: {
type: "number",
"default": 0,
minimum: 0,
units: "milliseconds"
}
};
var promoteId = {
"*": {
type: "string"
}
};
var v8Spec = {
$version: $version,
$root: $root,
sources: sources,
source: source,
source_vector: source_vector,
source_raster: source_raster,
source_raster_dem: source_raster_dem,
source_geojson: source_geojson,
source_video: source_video,
source_image: source_image,
layer: layer,
layout: layout,
layout_background: layout_background,
layout_fill: layout_fill,
layout_circle: layout_circle,
layout_heatmap: layout_heatmap,
"layout_fill-extrusion": {
visibility: {
type: "enum",
values: {
visible: {
},
none: {
}
},
"default": "visible",
"property-type": "constant"
}
},
layout_line: layout_line,
layout_symbol: layout_symbol,
layout_raster: layout_raster,
layout_hillshade: layout_hillshade,
"layout_color-relief": {
visibility: {
type: "enum",
values: {
visible: {
},
none: {
}
},
"default": "visible",
"property-type": "constant"
}
},
filter: filter,
filter_operator: filter_operator,
geometry_type: geometry_type,
"function": {
expression: {
type: "expression"
},
stops: {
type: "array",
value: "function_stop"
},
base: {
type: "number",
"default": 1,
minimum: 0
},
property: {
type: "string",
"default": "$zoom"
},
type: {
type: "enum",
values: {
identity: {
},
exponential: {
},
interval: {
},
categorical: {
}
},
"default": "exponential"
},
colorSpace: {
type: "enum",
values: {
rgb: {
},
lab: {
},
hcl: {
}
},
"default": "rgb"
},
"default": {
type: "*",
required: false
}
},
function_stop: function_stop,
expression: expression$1,
light: light,
sky: sky,
terrain: terrain,
projection: projection,
paint: paint,
paint_fill: paint_fill,
"paint_fill-extrusion": {
"fill-extrusion-opacity": {
type: "number",
"default": 1,
minimum: 0,
maximum: 1,
transition: true,
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"fill-extrusion-color": {
type: "color",
"default": "#000000",
transition: true,
requires: [
{
"!": "fill-extrusion-pattern"
}
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"fill-extrusion-translate": {
type: "array",
value: "number",
length: 2,
"default": [
0,
0
],
transition: true,
units: "pixels",
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"fill-extrusion-translate-anchor": {
type: "enum",
values: {
map: {
},
viewport: {
}
},
"default": "map",
requires: [
"fill-extrusion-translate"
],
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"fill-extrusion-pattern": {
type: "resolvedImage",
transition: true,
expression: {
interpolated: false,
parameters: [
"zoom",
"feature"
]
},
"property-type": "cross-faded-data-driven"
},
"fill-extrusion-height": {
type: "number",
"default": 0,
minimum: 0,
units: "meters",
transition: true,
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"fill-extrusion-base": {
type: "number",
"default": 0,
minimum: 0,
units: "meters",
transition: true,
requires: [
"fill-extrusion-height"
],
expression: {
interpolated: true,
parameters: [
"zoom",
"feature",
"feature-state"
]
},
"property-type": "data-driven"
},
"fill-extrusion-vertical-gradient": {
type: "boolean",
"default": true,
transition: false,
expression: {
interpolated: false,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
}
},
paint_line: paint_line,
paint_circle: paint_circle,
paint_heatmap: paint_heatmap,
paint_symbol: paint_symbol,
paint_raster: paint_raster,
paint_hillshade: paint_hillshade,
"paint_color-relief": {
"color-relief-opacity": {
type: "number",
"default": 1,
minimum: 0,
maximum: 1,
transition: true,
expression: {
interpolated: true,
parameters: [
"zoom"
]
},
"property-type": "data-constant"
},
"color-relief-color": {
type: "color",
transition: false,
expression: {
interpolated: true,
parameters: [
"elevation"
]
},
"property-type": "color-ramp"
}
},
paint_background: paint_background,
transition: transition,
"property-type": {
"data-driven": {
type: "property-type"
},
"cross-faded": {
type: "property-type"
},
"cross-faded-data-driven": {
type: "property-type"
},
"color-ramp": {
type: "property-type"
},
"data-constant": {
type: "property-type"
},
constant: {
type: "property-type"
}
},
promoteId: promoteId
};
const refProperties = ['type', 'source', 'source-layer', 'minzoom', 'maxzoom', 'filter', 'layout'];
function deref(layer, parent) {
const result = {};
for (const k in layer) {
if (k !== 'ref') {
result[k] = layer[k];
}
}
refProperties.forEach((k) => {
if (k in parent) {
result[k] = parent[k];
}
});
return result;
}
/**
*
* The input is not modified. The output may contain references to portions
* of the input.
*
* @param layers - array of layers, some of which may contain `ref` properties
* whose value is the `id` of another property
* @returns a new array where such layers have been augmented with the 'type', 'source', etc. properties
* from the parent layer, and the `ref` property has been removed.
*/
function derefLayers(layers) {
layers = layers.slice();
const map = Object.create(null);
for (let i = 0; i < layers.length; i++) {
map[layers[i].id] = layers[i];
}
for (let i = 0; i < layers.length; i++) {
if ('ref' in layers[i]) {
layers[i] = deref(layers[i], map[layers[i].ref]);
}
}
return layers;
}
/**
* Deeply compares two object literals.
*
* @private
*/
function deepEqual(a, b) {
if (Array.isArray(a)) {
if (!Array.isArray(b) || a.length !== b.length)
return false;
for (let i = 0; i < a.length; i++) {
if (!deepEqual(a[i], b[i]))
return false;
}
return true;
}
if (typeof a === 'object' && a !== null && b !== null) {
if (!(typeof b === 'object'))
return false;
const keys = Object.keys(a);
if (keys.length !== Object.keys(b).length)
return false;
for (const key in a) {
if (!deepEqual(a[key], b[key]))
return false;
}
return true;
}
return a === b;
}
/**
* The main reason for this method is to allow type check when adding a command to the array.
* @param commands - The commands array to add to
* @param command - The command to add
*/
function addCommand(commands, command) {
commands.push(command);
}
function addSource(sourceId, after, commands) {
addCommand(commands, { command: 'addSource', args: [sourceId, after[sourceId]] });
}
function removeSource(sourceId, commands, sourcesRemoved) {
addCommand(commands, { command: 'removeSource', args: [sourceId] });
sourcesRemoved[sourceId] = true;
}
function updateSource(sourceId, after, commands, sourcesRemoved) {
removeSource(sourceId, commands, sourcesRemoved);
addSource(sourceId, after, commands);
}
function canUpdateGeoJSON(before, after, sourceId) {
let prop;
for (prop in before[sourceId]) {
if (!Object.prototype.hasOwnProperty.call(before[sourceId], prop))
continue;
if (prop !== 'data' && !deepEqual(before[sourceId][prop], after[sourceId][prop])) {
return false;
}
}
for (prop in after[sourceId]) {
if (!Object.prototype.hasOwnProperty.call(after[sourceId], prop))
continue;
if (prop !== 'data' && !deepEqual(before[sourceId][prop], after[sourceId][prop])) {
return false;
}
}
return true;
}
function diffSources(before, after, commands, sourcesRemoved) {
before = before || {};
after = after || {};
let sourceId;
// look for sources to remove
for (sourceId in before) {
if (!Object.prototype.hasOwnProperty.call(before, sourceId))
continue;
if (!Object.prototype.hasOwnProperty.call(after, sourceId)) {
removeSource(sourceId, commands, sourcesRemoved);
}
}
// look for sources to add/update
for (sourceId in after) {
if (!Object.prototype.hasOwnProperty.call(after, sourceId))
continue;
if (!Object.prototype.hasOwnProperty.call(before, sourceId)) {
addSource(sourceId, after, commands);
}
else if (!deepEqual(before[sourceId], after[sourceId])) {
if (before[sourceId].type === 'geojson' && after[sourceId].type === 'geojson' && canUpdateGeoJSON(before, after, sourceId)) {
addCommand(commands, { command: 'setGeoJSONSourceData', args: [sourceId, after[sourceId].data] });
}
else {
// no update command, must remove then add
updateSource(sourceId, after, commands, sourcesRemoved);
}
}
}
}
function diffLayerPropertyChanges(before, after, commands, layerId, klass, command) {
before = before || {};
after = after || {};
for (const prop in before) {
if (!Object.prototype.hasOwnProperty.call(before, prop))
continue;
if (!deepEqual(before[prop], after[prop])) {
commands.push({ command, args: [layerId, prop, after[prop], klass] });
}
}
for (const prop in after) {
if (!Object.prototype.hasOwnProperty.call(after, prop) || Object.prototype.hasOwnProperty.call(before, prop))
continue;
if (!deepEqual(before[prop], after[prop])) {
commands.push({ command, args: [layerId, prop, after[prop], klass] });
}
}
}
function pluckId(layer) {
return layer.id;
}
function indexById(group, layer) {
group[layer.id] = layer;
return group;
}
function diffLayers(before, after, commands) {
before = before || [];
after = after || [];
// order of layers by id
const beforeOrder = before.map(pluckId);
const afterOrder = after.map(pluckId);
// index of layer by id
const beforeIndex = before.reduce(indexById, {});
const afterIndex = after.reduce(indexById, {});
// track order of layers as if they have been mutated
const tracker = beforeOrder.slice();
// layers that have been added do not need to be diffed
const clean = Object.create(null);
let layerId;
let beforeLayer;
let afterLayer;
let insertBeforeLayerId;
let prop;
// remove layers
for (let i = 0, d = 0; i < beforeOrder.length; i++) {
layerId = beforeOrder[i];
if (!Object.prototype.hasOwnProperty.call(afterIndex, layerId)) {
addCommand(commands, { command: 'removeLayer', args: [layerId] });
tracker.splice(tracker.indexOf(layerId, d), 1);
}
else {
// limit where in tracker we need to look for a match
d++;
}
}
// add/reorder layers
for (let i = 0, d = 0; i < afterOrder.length; i++) {
// work backwards as insert is before an existing layer
layerId = afterOrder[afterOrder.length - 1 - i];
if (tracker[tracker.length - 1 - i] === layerId)
continue;
if (Object.prototype.hasOwnProperty.call(beforeIndex, layerId)) {
// remove the layer before we insert at the correct position
addCommand(commands, { command: 'removeLayer', args: [layerId] });
tracker.splice(tracker.lastIndexOf(layerId, tracker.length - d), 1);
}
else {
// limit where in tracker we need to look for a match
d++;
}
// add layer at correct position
insertBeforeLayerId = tracker[tracker.length - i];
addCommand(commands, { command: 'addLayer', args: [afterIndex[layerId], insertBeforeLayerId] });
tracker.splice(tracker.length - i, 0, layerId);
clean[layerId] = true;
}
// update layers
for (let i = 0; i < afterOrder.length; i++) {
layerId = afterOrder[i];
beforeLayer = beforeIndex[layerId];
afterLayer = afterIndex[layerId];
// no need to update if previously added (new or moved)
if (clean[layerId] || deepEqual(beforeLayer, afterLayer))
continue;
// If source, source-layer, or type have changes, then remove the layer
// and add it back 'from scratch'.
if (!deepEqual(beforeLayer.source, afterLayer.source) || !deepEqual(beforeLayer['source-layer'], afterLayer['source-layer']) || !deepEqual(beforeLayer.type, afterLayer.type)) {
addCommand(commands, { command: 'removeLayer', args: [layerId] });
// we add the layer back at the same position it was already in, so
// there's no need to update the `tracker`
insertBeforeLayerId = tracker[tracker.lastIndexOf(layerId) + 1];
addCommand(commands, { command: 'addLayer', args: [afterLayer, insertBeforeLayerId] });
continue;
}
// layout, paint, filter, minzoom, maxzoom
diffLayerPropertyChanges(beforeLayer.layout, afterLayer.layout, commands, layerId, null, 'setLayoutProperty');
diffLayerPropertyChanges(beforeLayer.paint, afterLayer.paint, commands, layerId, null, 'setPaintProperty');
if (!deepEqual(beforeLayer.filter, afterLayer.filter)) {
addCommand(commands, { command: 'setFilter', args: [layerId, afterLayer.filter] });
}
if (!deepEqual(beforeLayer.minzoom, afterLayer.minzoom) || !deepEqual(beforeLayer.maxzoom, afterLayer.maxzoom)) {
addCommand(commands, { command: 'setLayerZoomRange', args: [layerId, afterLayer.minzoom, afterLayer.maxzoom] });
}
// handle all other layer props, including paint.*
for (prop in beforeLayer) {
if (!Object.prototype.hasOwnProperty.call(beforeLayer, prop))
continue;
if (prop === 'layout' || prop === 'paint' || prop === 'filter' ||
prop === 'metadata' || prop === 'minzoom' || prop === 'maxzoom')
continue;
if (prop.indexOf('paint.') === 0) {
diffLayerPropertyChanges(beforeLayer[prop], afterLayer[prop], commands, layerId, prop.slice(6), 'setPaintProperty');
}
else if (!deepEqual(beforeLayer[prop], afterLayer[prop])) {
addCommand(commands, { command: 'setLayerProperty', args: [layerId, prop, afterLayer[prop]] });
}
}
for (prop in afterLayer) {
if (!Object.prototype.hasOwnProperty.call(afterLayer, prop) || Object.prototype.hasOwnProperty.call(beforeLayer, prop))
continue;
if (prop === 'layout' || prop === 'paint' || prop === 'filter' ||
prop === 'metadata' || prop === 'minzoom' || prop === 'maxzoom')
continue;
if (prop.indexOf('paint.') === 0) {
diffLayerPropertyChanges(beforeLayer[prop], afterLayer[prop], commands, layerId, prop.slice(6), 'setPaintProperty');
}
else if (!deepEqual(beforeLayer[prop], afterLayer[prop])) {
addCommand(commands, { command: 'setLayerProperty', args: [layerId, prop, afterLayer[prop]] });
}
}
}
}
/**
* Diff two stylesheet
*
* Creates semanticly aware diffs that can easily be applied at runtime.
* Operations produced by the diff closely resemble the maplibre-gl-js API. Any
* error creating the diff will fall back to the 'setStyle' operation.
*
* Example diff:
* [
* { command: 'setConstant', args: ['@water', '#0000FF'] },
* { command: 'setPaintProperty', args: ['background', 'background-color', 'black'] }
* ]
*
* @private
* @param {*} [before] stylesheet to compare from
* @param {*} after stylesheet to compare to
* @returns Array list of changes
*/
function diff(before, after) {
if (!before)
return [{ command: 'setStyle', args: [after] }];
let commands = [];
try {
// Handle changes to top-level properties
if (!deepEqual(before.version, after.version)) {
return [{ command: 'setStyle', args: [after] }];
}
if (!deepEqual(before.center, after.center)) {
commands.push({ command: 'setCenter', args: [after.center] });
}
if (!deepEqual(before.state, after.state)) {
commands.push({ command: 'setGlobalState', args: [after.state] });
}
if (!deepEqual(before.centerAltitude, after.centerAltitude)) {
commands.push({ command: 'setCenterAltitude', args: [after.centerAltitude] });
}
if (!deepEqual(before.zoom, after.zoom)) {
commands.push({ command: 'setZoom', args: [after.zoom] });
}
if (!deepEqual(before.bearing, after.bearing)) {
commands.push({ command: 'setBearing', args: [after.bearing] });
}
if (!deepEqual(before.pitch, after.pitch)) {
commands.push({ command: 'setPitch', args: [after.pitch] });
}
if (!deepEqual(before.roll, after.roll)) {
commands.push({ command: 'setRoll', args: [after.roll] });
}
if (!deepEqual(before.sprite, after.sprite)) {
commands.push({ command: 'setSprite', args: [after.sprite] });
}
if (!deepEqual(before.glyphs, after.glyphs)) {
commands.push({ command: 'setGlyphs', args: [after.glyphs] });
}
if (!deepEqual(before.transition, after.transition)) {
commands.push({ command: 'setTransition', args: [after.transition] });
}
if (!deepEqual(before.light, after.light)) {
commands.push({ command: 'setLight', args: [after.light] });
}
if (!deepEqual(before.terrain, after.terrain)) {
commands.push({ command: 'setTerrain', args: [after.terrain] });
}
if (!deepEqual(before.sky, after.sky)) {
commands.push({ command: 'setSky', args: [after.sky] });
}
if (!deepEqual(before.projection, after.projection)) {
commands.push({ command: 'setProjection', args: [after.projection] });
}
// Handle changes to `sources`
// If a source is to be removed, we also--before the removeSource
// command--need to remove all the style layers that depend on it.
const sourcesRemoved = {};
// First collect the {add,remove}Source commands
const removeOrAddSourceCommands = [];
diffSources(before.sources, after.sources, removeOrAddSourceCommands, sourcesRemoved);
// Push a removeLayer command for each style layer that depends on a
// source that's being removed.
// Also, exclude any such layers them from the input to `diffLayers`
// below, so that diffLayers produces the appropriate `addLayers`
// command
const beforeLayers = [];
if (before.layers) {
before.layers.forEach((layer) => {
if ('source' in layer && sourcesRemoved[layer.source]) {
commands.push({ command: 'removeLayer', args: [layer.id] });
}
else {
beforeLayers.push(layer);
}
});
}
commands = commands.concat(removeOrAddSourceCommands);
// Handle changes to `layers`
diffLayers(beforeLayers, after.layers, commands);
}
catch (e) {
// fall back to setStyle
console.warn('Unable to compute style diff:', e);
commands = [{ command: 'setStyle', args: [after] }];
}
return commands;
}
// Note: Do not inherit from Error. It breaks when transpiling to ES5.
class ValidationError {
constructor(key, value, message, identifier) {
this.message = (key ? `${key}: ` : '') + message;
if (identifier)
this.identifier = identifier;
if (value !== null && value !== undefined && value.__line__) {
this.line = value.__line__;
}
}
}
// Note: Do not inherit from Error. It breaks when transpiling to ES5.
class ParsingError {
constructor(error) {
this.error = error;
this.message = error.message;
const match = error.message.match(/line (\d+)/);
this.line = match ? parseInt(match[1], 10) : 0;
}
}
function extendBy(output, ...inputs) {
for (const input of inputs) {
for (const k in input) {
output[k] = input[k];
}
}
return output;
}
class ExpressionParsingError extends Error {
constructor(key, message) {
super(message);
this.message = message;
this.key = key;
}
}
/**
* Tracks `let` bindings during expression parsing.
* @private
*/
class Scope {
constructor(parent, bindings = []) {
this.parent = parent;
this.bindings = {};
for (const [name, expression] of bindings) {
this.bindings[name] = expression;
}
}
concat(bindings) {
return new Scope(this, bindings);
}
get(name) {
if (this.bindings[name]) {
return this.bindings[name];
}
if (this.parent) {
return this.parent.get(name);
}
throw new Error(`${name} not found in scope.`);
}
has(name) {
if (this.bindings[name])
return true;
return this.parent ? this.parent.has(name) : false;
}
}
const NullType = { kind: 'null' };
const NumberType = { kind: 'number' };
const StringType = { kind: 'string' };
const BooleanType = { kind: 'boolean' };
const ColorType = { kind: 'color' };
const ProjectionDefinitionType = { kind: 'projectionDefinition' };
const ObjectType = { kind: 'object' };
const ValueType = { kind: 'value' };
const ErrorType = { kind: 'error' };
const CollatorType = { kind: 'collator' };
const FormattedType = { kind: 'formatted' };
const PaddingType = { kind: 'padding' };
const ColorArrayType = { kind: 'colorArray' };
const NumberArrayType = { kind: 'numberArray' };
const ResolvedImageType = { kind: 'resolvedImage' };
const VariableAnchorOffsetCollectionType = { kind: 'variableAnchorOffsetCollection' };
function array(itemType, N) {
return {
kind: 'array',
itemType,
N
};
}
function typeToString(type) {
if (type.kind === 'array') {
const itemType = typeToString(type.itemType);
return typeof type.N === 'number' ?
`array<${itemType}, ${type.N}>` :
type.itemType.kind === 'value' ? 'array' : `array<${itemType}>`;
}
else {
return type.kind;
}
}
const valueMemberTypes = [
NullType,
NumberType,
StringType,
BooleanType,
ColorType,
ProjectionDefinitionType,
FormattedType,
ObjectType,
array(ValueType),
PaddingType,
NumberArrayType,
ColorArrayType,
ResolvedImageType,
VariableAnchorOffsetCollectionType
];
/**
* Returns null if `t` is a subtype of `expected`; otherwise returns an
* error message.
* @private
*/
function checkSubtype(expected, t) {
if (t.kind === 'error') {
// Error is a subtype of every type
return null;
}
else if (expected.kind === 'array') {
if (t.kind === 'array' &&
((t.N === 0 && t.itemType.kind === 'value') || !checkSubtype(expected.itemType, t.itemType)) &&
(typeof expected.N !== 'number' || expected.N === t.N)) {
return null;
}
}
else if (expected.kind === t.kind) {
return null;
}
else if (expected.kind === 'value') {
for (const memberType of valueMemberTypes) {
if (!checkSubtype(memberType, t)) {
return null;
}
}
}
return `Expected ${typeToString(expected)} but found ${typeToString(t)} instead.`;
}
function isValidType(provided, allowedTypes) {
return allowedTypes.some(t => t.kind === provided.kind);
}
function isValidNativeType(provided, allowedTypes) {
return allowedTypes.some(t => {
if (t === 'null') {
return provided === null;
}
else if (t === 'array') {
return Array.isArray(provided);
}
else if (t === 'object') {
return provided && !Array.isArray(provided) && typeof provided === 'object';
}
else {
return t === typeof provided;
}
});
}
/**
* Verify whether the specified type is of the same type as the specified sample.
*
* @param provided Type to verify
* @param sample Sample type to reference
* @returns `true` if both objects are of the same type, `false` otherwise
* @example basic types
* if (verifyType(outputType, ValueType)) {
* // type narrowed to:
* outputType.kind; // 'value'
* }
* @example array types
* if (verifyType(outputType, array(NumberType))) {
* // type narrowed to:
* outputType.kind; // 'array'
* outputType.itemType; // NumberTypeT
* outputType.itemType.kind; // 'number'
* }
*/
function verifyType(provided, sample) {
if (provided.kind === 'array' && sample.kind === 'array') {
return provided.itemType.kind === sample.itemType.kind && typeof provided.N === 'number';
}
return provided.kind === sample.kind;
}
// See https://observablehq.com/@mbostock/lab-and-rgb
const Xn = 0.96422, Yn = 1, Zn = 0.82521, t0 = 4 / 29, t1 = 6 / 29, t2 = 3 * t1 * t1, t3 = t1 * t1 * t1, deg2rad = Math.PI / 180, rad2deg = 180 / Math.PI;
function constrainAngle(angle) {
angle = angle % 360;
if (angle < 0) {
angle += 360;
}
return angle;
}
function rgbToLab([r, g, b, alpha]) {
r = rgb2xyz(r);
g = rgb2xyz(g);
b = rgb2xyz(b);
let x, z;
const y = xyz2lab((0.2225045 * r + 0.7168786 * g + 0.0606169 * b) / Yn);
if (r === g && g === b) {
x = z = y;
}
else {
x = xyz2lab((0.4360747 * r + 0.3850649 * g + 0.1430804 * b) / Xn);
z = xyz2lab((0.0139322 * r + 0.0971045 * g + 0.7141733 * b) / Zn);
}
const l = 116 * y - 16;
return [(l < 0) ? 0 : l, 500 * (x - y), 200 * (y - z), alpha];
}
function rgb2xyz(x) {
return (x <= 0.04045) ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
}
function xyz2lab(t) {
return (t > t3) ? Math.pow(t, 1 / 3) : t / t2 + t0;
}
function labToRgb([l, a, b, alpha]) {
let y = (l + 16) / 116, x = isNaN(a) ? y : y + a / 500, z = isNaN(b) ? y : y - b / 200;
y = Yn * lab2xyz(y);
x = Xn * lab2xyz(x);
z = Zn * lab2xyz(z);
return [
xyz2rgb(3.1338561 * x - 1.6168667 * y - 0.4906146 * z), // D50 -> sRGB
xyz2rgb(-0.9787684 * x + 1.9161415 * y + 0.0334540 * z),
xyz2rgb(0.0719453 * x - 0.2289914 * y + 1.4052427 * z),
alpha,
];
}
function xyz2rgb(x) {
x = (x <= 0.00304) ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055;
return (x < 0) ? 0 : (x > 1) ? 1 : x; // clip to 0..1 range
}
function lab2xyz(t) {
return (t > t1) ? t * t * t : t2 * (t - t0);
}
function rgbToHcl(rgbColor) {
const [l, a, b, alpha] = rgbToLab(rgbColor);
const c = Math.sqrt(a * a + b * b);
const h = Math.round(c * 10000) ? constrainAngle(Math.atan2(b, a) * rad2deg) : NaN;
return [h, c, l, alpha];
}
function hclToRgb([h, c, l, alpha]) {
h = isNaN(h) ? 0 : h * deg2rad;
return labToRgb([l, Math.cos(h) * c, Math.sin(h) * c, alpha]);
}
// https://drafts.csswg.org/css-color-4/#hsl-to-rgb
function hslToRgb([h, s, l, alpha]) {
h = constrainAngle(h);
s /= 100;
l /= 100;
function f(n) {
const k = (n + h / 30) % 12;
const a = s * Math.min(l, 1 - l);
return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
}
return [f(0), f(8), f(4), alpha];
}
// polyfill for Object.hasOwn
const hasOwnProperty = Object.hasOwn ||
function hasOwnProperty(object, key) {
return Object.prototype.hasOwnProperty.call(object, key);
};
function getOwn(object, key) {
return hasOwnProperty(object, key) ? object[key] : undefined;
}
/**
* CSS color parser compliant with CSS Color 4 Specification.
* Supports: named colors, `transparent` keyword, all rgb hex notations,
* rgb(), rgba(), hsl() and hsla() functions.
* Does not round the parsed values to integers from the range 0..255.
*
* Syntax:
*
* <alpha-value> = <number> | <percentage>
* <hue> = <number> | <angle>
*
* rgb() = rgb( <percentage>{3} [ / <alpha-value> ]? ) | rgb( <number>{3} [ / <alpha-value> ]? )
* rgb() = rgb( <percentage>#{3} , <alpha-value>? ) | rgb( <number>#{3} , <alpha-value>? )
*
* hsl() = hsl( <hue> <percentage> <percentage> [ / <alpha-value> ]? )
* hsl() = hsl( <hue>, <percentage>, <percentage>, <alpha-value>? )
*
* Caveats:
* - <angle> - <number> with optional `deg` suffix; `grad`, `rad`, `turn` are not supported
* - `none` keyword is not supported
* - comments inside rgb()/hsl() are not supported
* - legacy color syntax rgba() is supported with an identical grammar and behavior to rgb()
* - legacy color syntax hsla() is supported with an identical grammar and behavior to hsl()
*
* @param input CSS color string to parse.
* @returns Color in sRGB color space, with `red`, `green`, `blue`
* and `alpha` channels normalized to the range 0..1,
* or `undefined` if the input is not a valid color string.
*/
function parseCssColor(input) {
input = input.toLowerCase().trim();
if (input === 'transparent') {
return [0, 0, 0, 0];
}
// 'white', 'black', 'blue'
const namedColorsMatch = getOwn(namedColors, input);
if (namedColorsMatch) {
const [r, g, b] = namedColorsMatch;
return [r / 255, g / 255, b / 255, 1];
}
// #f0c, #f0cf, #ff00cc, #ff00ccff
if (input.startsWith('#')) {
const hexRegexp = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/;
if (hexRegexp.test(input)) {
const step = input.length < 6 ? 1 : 2;
let i = 1;
return [
parseHex(input.slice(i, i += step)),
parseHex(input.slice(i, i += step)),
parseHex(input.slice(i, i += step)),
parseHex(input.slice(i, i + step) || 'ff'),
];
}
}
// rgb(128 0 0), rgb(50% 0% 0%), rgba(255,0,255,0.6), rgb(255 0 255 / 60%), rgb(100% 0% 100% /.6)
if (input.startsWith('rgb')) {
const rgbRegExp = /^rgba?\(\s*([\de.+-]+)(%)?(?:\s+|\s*(,)\s*)([\de.+-]+)(%)?(?:\s+|\s*(,)\s*)([\de.+-]+)(%)?(?:\s*([,\/])\s*([\de.+-]+)(%)?)?\s*\)$/;
const rgbMatch = input.match(rgbRegExp);
if (rgbMatch) {
const [_, // eslint-disable-line @typescript-eslint/no-unused-vars
r, // <numeric>
rp, // % (optional)
f1, // , (optional)
g, // <numeric>
gp, // % (optional)
f2, // , (optional)
b, // <numeric>
bp, // % (optional)
f3, // ,|/ (optional)
a, // <numeric> (optional)
ap, // % (optional)
] = rgbMatch;
const argFormat = [f1 || ' ', f2 || ' ', f3].join('');
if (argFormat === ' ' ||
argFormat === ' /' ||
argFormat === ',,' ||
argFormat === ',,,') {
const valFormat = [rp, gp, bp].join('');
const maxValue = (valFormat === '%%%') ? 100 :
(valFormat === '') ? 255 : 0;
if (maxValue) {
const rgba = [
clamp(+r / maxValue, 0, 1),
clamp(+g / maxValue, 0, 1),
clamp(+b / maxValue, 0, 1),
a ? parseAlpha(+a, ap) : 1,
];
if (validateNumbers(rgba)) {
return rgba;
}
// invalid numbers
}
// values must be all numbers or all percentages
}
return; // comma optional syntax requires no commas at all
}
}
// hsl(120 50% 80%), hsla(120deg,50%,80%,.9), hsl(12e1 50% 80% / 90%)
const hslRegExp = /^hsla?\(\s*([\de.+-]+)(?:deg)?(?:\s+|\s*(,)\s*)([\de.+-]+)%(?:\s+|\s*(,)\s*)([\de.+-]+)%(?:\s*([,\/])\s*([\de.+-]+)(%)?)?\s*\)$/;
const hslMatch = input.match(hslRegExp);
if (hslMatch) {
const [_, // eslint-disable-line @typescript-eslint/no-unused-vars
h, // <numeric>
f1, // , (optional)
s, // <numeric>
f2, // , (optional)
l, // <numeric>
f3, // ,|/ (optional)
a, // <numeric> (optional)
ap, // % (optional)
] = hslMatch;
const argFormat = [f1 || ' ', f2 || ' ', f3].join('');
if (argFormat === ' ' ||
argFormat === ' /' ||
argFormat === ',,' ||
argFormat === ',,,') {
const hsla = [
+h,
clamp(+s, 0, 100),
clamp(+l, 0, 100),
a ? parseAlpha(+a, ap) : 1,
];
if (validateNumbers(hsla)) {
return hslToRgb(hsla);
}
// invalid numbers
}
// comma optional syntax requires no commas at all
}
}
function parseHex(hex) {
return parseInt(hex.padEnd(2, hex), 16) / 255;
}
function parseAlpha(a, asPercentage) {
return clamp(asPercentage ? (a / 100) : a, 0, 1);
}
function clamp(n, min, max) {
return Math.min(Math.max(min, n), max);
}
/**
* The regular expression for numeric values is not super specific, and it may
* happen that it will accept a value that is not a valid number. In order to
* detect and eliminate such values this function exists.
*
* @param array Array of uncertain numbers.
* @returns `true` if the specified array contains only valid numbers, `false` otherwise.
*/
function validateNumbers(array) {
return !array.some(Number.isNaN);
}
/**
* To generate:
* - visit {@link https://www.w3.org/TR/css-color-4/#named-colors}
* - run in the console:
* @example
* copy(`{\n${[...document.querySelector('.named-color-table tbody').children].map((tr) => `${tr.cells[2].textContent.trim()}: [${tr.cells[4].textContent.trim().split(/\s+/).join(', ')}],`).join('\n')}\n}`);
*/
const namedColors = {
aliceblue: [240, 248, 255],
antiquewhite: [250, 235, 215],
aqua: [0, 255, 255],
aquamarine: [127, 255, 212],
azure: [240, 255, 255],
beige: [245, 245, 220],
bisque: [255, 228, 196],
black: [0, 0, 0],
blanchedalmond: [255, 235, 205],
blue: [0, 0, 255],
blueviolet: [138, 43, 226],
brown: [165, 42, 42],
burlywood: [222, 184, 135],
cadetblue: [95, 158, 160],
chartreuse: [127, 255, 0],
chocolate: [210, 105, 30],
coral: [255, 127, 80],
cornflowerblue: [100, 149, 237],
cornsilk: [255, 248, 220],
crimson: [220, 20, 60],
cyan: [0, 255, 255],
darkblue: [0, 0, 139],
darkcyan: [0, 139, 139],
darkgoldenrod: [184, 134, 11],
darkgray: [169, 169, 169],
darkgreen: [0, 100, 0],
darkgrey: [169, 169, 169],
darkkhaki: [189, 183, 107],
darkmagenta: [139, 0, 139],
darkolivegreen: [85, 107, 47],
darkorange: [255, 140, 0],
darkorchid: [153, 50, 204],
darkred: [139, 0, 0],
darksalmon: [233, 150, 122],
darkseagreen: [143, 188, 143],
darkslateblue: [72, 61, 139],
darkslategray: [47, 79, 79],
darkslategrey: [47, 79, 79],
darkturquoise: [0, 206, 209],
darkviolet: [148, 0, 211],
deeppink: [255, 20, 147],
deepskyblue: [0, 191, 255],
dimgray: [105, 105, 105],
dimgrey: [105, 105, 105],
dodgerblue: [30, 144, 255],
firebrick: [178, 34, 34],
floralwhite: [255, 250, 240],
forestgreen: [34, 139, 34],
fuchsia: [255, 0, 255],
gainsboro: [220, 220, 220],
ghostwhite: [248, 248, 255],
gold: [255, 215, 0],
goldenrod: [218, 165, 32],
gray: [128, 128, 128],
green: [0, 128, 0],
greenyellow: [173, 255, 47],
grey: [128, 128, 128],
honeydew: [240, 255, 240],
hotpink: [255, 105, 180],
indianred: [205, 92, 92],
indigo: [75, 0, 130],
ivory: [255, 255, 240],
khaki: [240, 230, 140],
lavender: [230, 230, 250],
lavenderblush: [255, 240, 245],
lawngreen: [124, 252, 0],
lemonchiffon: [255, 250, 205],
lightblue: [173, 216, 230],
lightcoral: [240, 128, 128],
lightcyan: [224, 255, 255],
lightgoldenrodyellow: [250, 250, 210],
lightgray: [211, 211, 211],
lightgreen: [144, 238, 144],
lightgrey: [211, 211, 211],
lightpink: [255, 182, 193],
lightsalmon: [255, 160, 122],
lightseagreen: [32, 178, 170],
lightskyblue: [135, 206, 250],
lightslategray: [119, 136, 153],
lightslategrey: [119, 136, 153],
lightsteelblue: [176, 196, 222],
lightyellow: [255, 255, 224],
lime: [0, 255, 0],
limegreen: [50, 205, 50],
linen: [250, 240, 230],
magenta: [255, 0, 255],
maroon: [128, 0, 0],
mediumaquamarine: [102, 205, 170],
mediumblue: [0, 0, 205],
mediumorchid: [186, 85, 211],
mediumpurple: [147, 112, 219],
mediumseagreen: [60, 179, 113],
mediumslateblue: [123, 104, 238],
mediumspringgreen: [0, 250, 154],
mediumturquoise: [72, 209, 204],
mediumvioletred: [199, 21, 133],
midnightblue: [25, 25, 112],
mintcream: [245, 255, 250],
mistyrose: [255, 228, 225],
moccasin: [255, 228, 181],
navajowhite: [255, 222, 173],
navy: [0, 0, 128],
oldlace: [253, 245, 230],
olive: [128, 128, 0],
olivedrab: [107, 142, 35],
orange: [255, 165, 0],
orangered: [255, 69, 0],
orchid: [218, 112, 214],
palegoldenrod: [238, 232, 170],
palegreen: [152, 251, 152],
paleturquoise: [175, 238, 238],
palevioletred: [219, 112, 147],
papayawhip: [255, 239, 213],
peachpuff: [255, 218, 185],
peru: [205, 133, 63],
pink: [255, 192, 203],
plum: [221, 160, 221],
powderblue: [176, 224, 230],
purple: [128, 0, 128],
rebeccapurple: [102, 51, 153],
red: [255, 0, 0],
rosybrown: [188, 143, 143],
royalblue: [65, 105, 225],
saddlebrown: [139, 69, 19],
salmon: [250, 128, 114],
sandybrown: [244, 164, 96],
seagreen: [46, 139, 87],
seashell: [255, 245, 238],
sienna: [160, 82, 45],
silver: [192, 192, 192],
skyblue: [135, 206, 235],
slateblue: [106, 90, 205],
slategray: [112, 128, 144],
slategrey: [112, 128, 144],
snow: [255, 250, 250],
springgreen: [0, 255, 127],
steelblue: [70, 130, 180],
tan: [210, 180, 140],
teal: [0, 128, 128],
thistle: [216, 191, 216],
tomato: [255, 99, 71],
turquoise: [64, 224, 208],
violet: [238, 130, 238],
wheat: [245, 222, 179],
white: [255, 255, 255],
whitesmoke: [245, 245, 245],
yellow: [255, 255, 0],
yellowgreen: [154, 205, 50],
};
function interpolateNumber(from, to, t) {
return from + t * (to - from);
}
function interpolateArray(from, to, t) {
return from.map((d, i) => {
return interpolateNumber(d, to[i], t);
});
}
/**
* Checks whether the specified color space is one of the supported interpolation color spaces.
*
* @param colorSpace Color space key to verify.
* @returns `true` if the specified color space is one of the supported
* interpolation color spaces, `false` otherwise
*/
function isSupportedInterpolationColorSpace(colorSpace) {
return colorSpace === 'rgb' || colorSpace === 'hcl' || colorSpace === 'lab';
}
/**
* Color representation used by WebGL.
* Defined in sRGB color space and pre-blended with alpha.
* @private
*/
class Color {
/**
* @param r Red component premultiplied by `alpha` 0..1
* @param g Green component premultiplied by `alpha` 0..1
* @param b Blue component premultiplied by `alpha` 0..1
* @param [alpha=1] Alpha component 0..1
* @param [premultiplied=true] Whether the `r`, `g` and `b` values have already
* been multiplied by alpha. If `true` nothing happens if `false` then they will
* be multiplied automatically.
*/
constructor(r, g, b, alpha = 1, premultiplied = true) {
this.r = r;
this.g = g;
this.b = b;
this.a = alpha;
if (!premultiplied) {
this.r *= alpha;
this.g *= alpha;
this.b *= alpha;
if (!alpha) {
// alpha = 0 erases completely rgb channels. This behavior is not desirable
// if this particular color is later used in color interpolation.
// Because of that, a reference to original color is saved.
this.overwriteGetter('rgb', [r, g, b, alpha]);
}
}
}
/**
* Parses CSS color strings and converts colors to sRGB color space if needed.
* Officially supported color formats:
* - keyword, e.g. 'aquamarine' or 'steelblue'
* - hex (with 3, 4, 6 or 8 digits), e.g. '#f0f' or '#e9bebea9'
* - rgb and rgba, e.g. 'rgb(0,240,120)' or 'rgba(0%,94%,47%,0.1)' or 'rgb(0 240 120 / .3)'
* - hsl and hsla, e.g. 'hsl(0,0%,83%)' or 'hsla(0,0%,83%,.5)' or 'hsl(0 0% 83% / 20%)'
*
* @param input CSS color string to parse.
* @returns A `Color` instance, or `undefined` if the input is not a valid color string.
*/
static parse(input) {
// in zoom-and-property function input could be an instance of Color class
if (input instanceof Color) {
return input;
}
if (typeof input !== 'string') {
return;
}
const rgba = parseCssColor(input);
if (rgba) {
return new Color(...rgba, false);
}
}
/**
* Used in color interpolation and by 'to-rgba' expression.
*
* @returns Gien color, with reversed alpha blending, in sRGB color space.
*/
get rgb() {
const { r, g, b, a } = this;
const f = a || Infinity; // reverse alpha blending factor
return this.overwriteGetter('rgb', [r / f, g / f, b / f, a]);
}
/**
* Used in color interpolation.
*
* @returns Gien color, with reversed alpha blending, in HCL color space.
*/
get hcl() {
return this.overwriteGetter('hcl', rgbToHcl(this.rgb));
}
/**
* Used in color interpolation.
*
* @returns Gien color, with reversed alpha blending, in LAB color space.
*/
get lab() {
return this.overwriteGetter('lab', rgbToLab(this.rgb));
}
/**
* Lazy getter pattern. When getter is called for the first time lazy value
* is calculated and then overwrites getter function in given object instance.
*
* @example:
* const redColor = Color.parse('red');
* let x = redColor.hcl; // this will invoke `get hcl()`, which will calculate
* // the value of red in HCL space and invoke this `overwriteGetter` function
* // which in turn will set a field with a key 'hcl' in the `redColor` object.
* // In other words it will override `get hcl()` from its `Color` prototype
* // with its own property: hcl = [calculated red value in hcl].
* let y = redColor.hcl; // next call will no longer invoke getter but simply
* // return the previously calculated value
* x === y; // true - `x` is exactly the same object as `y`
*
* @param getterKey Getter key
* @param lazyValue Lazily calculated value to be memoized by current instance
* @private
*/
overwriteGetter(getterKey, lazyValue) {
Object.defineProperty(this, getterKey, { value: lazyValue });
return lazyValue;
}
/**
* Used by 'to-string' expression.
*
* @returns Serialized color in format `rgba(r,g,b,a)`
* where r,g,b are numbers within 0..255 and alpha is number within 1..0
*
* @example
* var purple = new Color.parse('purple');
* purple.toString; // = "rgba(128,0,128,1)"
* var translucentGreen = new Color.parse('rgba(26, 207, 26, .73)');
* translucentGreen.toString(); // = "rgba(26,207,26,0.73)"
*/
toString() {
const [r, g, b, a] = this.rgb;
return `rgba(${[r, g, b].map(n => Math.round(n * 255)).join(',')},${a})`;
}
static interpolate(from, to, t, spaceKey = 'rgb') {
switch (spaceKey) {
case 'rgb': {
const [r, g, b, alpha] = interpolateArray(from.rgb, to.rgb, t);
return new Color(r, g, b, alpha, false);
}
case 'hcl': {
const [hue0, chroma0, light0, alphaF] = from.hcl;
const [hue1, chroma1, light1, alphaT] = to.hcl;
// https://github.com/gka/chroma.js/blob/cd1b3c0926c7a85cbdc3b1453b3a94006de91a92/src/interpolator/_hsx.js
let hue, chroma;
if (!isNaN(hue0) && !isNaN(hue1)) {
let dh = hue1 - hue0;
if (hue1 > hue0 && dh > 180) {
dh -= 360;
}
else if (hue1 < hue0 && hue0 - hue1 > 180) {
dh += 360;
}
hue = hue0 + t * dh;
}
else if (!isNaN(hue0)) {
hue = hue0;
if (light1 === 1 || light1 === 0)
chroma = chroma0;
}
else if (!isNaN(hue1)) {
hue = hue1;
if (light0 === 1 || light0 === 0)
chroma = chroma1;
}
else {
hue = NaN;
}
const [r, g, b, alpha] = hclToRgb([
hue,
chroma !== null && chroma !== void 0 ? chroma : interpolateNumber(chroma0, chroma1, t),
interpolateNumber(light0, light1, t),
interpolateNumber(alphaF, alphaT, t),
]);
return new Color(r, g, b, alpha, false);
}
case 'lab': {
const [r, g, b, alpha] = labToRgb(interpolateArray(from.lab, to.lab, t));
return new Color(r, g, b, alpha, false);
}
}
}
}
Color.black = new Color(0, 0, 0, 1);
Color.white = new Color(1, 1, 1, 1);
Color.transparent = new Color(0, 0, 0, 0);
Color.red = new Color(1, 0, 0, 1);
class Collator {
constructor(caseSensitive, diacriticSensitive, locale) {
if (caseSensitive)
this.sensitivity = diacriticSensitive ? 'variant' : 'case';
else
this.sensitivity = diacriticSensitive ? 'accent' : 'base';
this.locale = locale;
this.collator = new Intl.Collator(this.locale ? this.locale : [], { sensitivity: this.sensitivity, usage: 'search' });
}
compare(lhs, rhs) {
return this.collator.compare(lhs, rhs);
}
resolvedLocale() {
// We create a Collator without "usage: search" because we don't want
// the search options encoded in our result (e.g. "en-u-co-search")
return new Intl.Collator(this.locale ? this.locale : [])
.resolvedOptions().locale;
}
}
const VERTICAL_ALIGN_OPTIONS = ['bottom', 'center', 'top'];
class FormattedSection {
constructor(text, image, scale, fontStack, textColor, verticalAlign) {
this.text = text;
this.image = image;
this.scale = scale;
this.fontStack = fontStack;
this.textColor = textColor;
this.verticalAlign = verticalAlign;
}
}
class Formatted {
constructor(sections) {
this.sections = sections;
}
static fromString(unformatted) {
return new Formatted([new FormattedSection(unformatted, null, null, null, null, null)]);
}
isEmpty() {
if (this.sections.length === 0)
return true;
return !this.sections.some(section => section.text.length !== 0 ||
(section.image && section.image.name.length !== 0));
}
static factory(text) {
if (text instanceof Formatted) {
return text;
}
else {
return Formatted.fromString(text);
}
}
toString() {
if (this.sections.length === 0)
return '';
return this.sections.map(section => section.text).join('');
}
}
/**
* A set of four numbers representing padding around a box. Create instances from
* bare arrays or numeric values using the static method `Padding.parse`.
* @private
*/
class Padding {
constructor(values) {
this.values = values.slice();
}
/**
* Numeric padding values
* @param input A padding value
* @returns A `Padding` instance, or `undefined` if the input is not a valid padding value.
*/
static parse(input) {
if (input instanceof Padding) {
return input;
}
// Backwards compatibility: bare number is treated the same as array with single value.
// Padding applies to all four sides.
if (typeof input === 'number') {
return new Padding([input, input, input, input]);
}
if (!Array.isArray(input)) {
return undefined;
}
if (input.length < 1 || input.length > 4) {
return undefined;
}
for (const val of input) {
if (typeof val !== 'number') {
return undefined;
}
}
// Expand shortcut properties into explicit 4-sided values
switch (input.length) {
case 1:
input = [input[0], input[0], input[0], input[0]];
break;
case 2:
input = [input[0], input[1], input[0], input[1]];
break;
case 3:
input = [input[0], input[1], input[2], input[1]];
break;
}
return new Padding(input);
}
toString() {
return JSON.stringify(this.values);
}
static interpolate(from, to, t) {
return new Padding(interpolateArray(from.values, to.values, t));
}
}
/**
* An array of numbers. Create instances from
* bare arrays or numeric values using the static method `NumberArray.parse`.
* @private
*/
class NumberArray {
constructor(values) {
this.values = values.slice();
}
/**
* Numeric NumberArray values
* @param input A NumberArray value
* @returns A `NumberArray` instance, or `undefined` if the input is not a valid NumberArray value.
*/
static parse(input) {
if (input instanceof NumberArray) {
return input;
}
// Backwards compatibility (e.g. hillshade-illumination-direction): bare number is treated the same as array with single value.
if (typeof input === 'number') {
return new NumberArray([input]);
}
if (!Array.isArray(input)) {
return undefined;
}
for (const val of input) {
if (typeof val !== 'number') {
return undefined;
}
}
return new NumberArray(input);
}
toString() {
return JSON.stringify(this.values);
}
static interpolate(from, to, t) {
return new NumberArray(interpolateArray(from.values, to.values, t));
}
}
/**
* An array of colors. Create instances from
* bare arrays or strings using the static method `ColorArray.parse`.
* @private
*/
class ColorArray {
constructor(values) {
this.values = values.slice();
}
/**
* ColorArray values
* @param input A ColorArray value
* @returns A `ColorArray` instance, or `undefined` if the input is not a valid ColorArray value.
*/
static parse(input) {
if (input instanceof ColorArray) {
return input;
}
// Backwards compatibility (e.g. hillshade-shadow-color): bare Color is treated the same as array with single value.
if (typeof input === 'string') {
const parsed_val = Color.parse(input);
if (!parsed_val) {
return undefined;
}
return new ColorArray([parsed_val]);
}
if (!Array.isArray(input)) {
return undefined;
}
const colors = [];
for (const val of input) {
if (typeof val !== 'string') {
return undefined;
}
const parsed_val = Color.parse(val);
if (!parsed_val) {
return undefined;
}
colors.push(parsed_val);
}
return new ColorArray(colors);
}
toString() {
return JSON.stringify(this.values);
}
static interpolate(from, to, t, spaceKey = 'rgb') {
const colors = [];
if (from.values.length != to.values.length) {
throw new Error(`colorArray: Arrays have mismatched length (${from.values.length} vs. ${to.values.length}), cannot interpolate.`);
}
for (let i = 0; i < from.values.length; i++) {
colors.push(Color.interpolate(from.values[i], to.values[i], t, spaceKey));
}
return new ColorArray(colors);
}
}
class RuntimeError extends Error {
constructor(message) {
super(message);
this.name = 'RuntimeError';
}
toJSON() {
return this.message;
}
}
/** Set of valid anchor positions, as a set for validation */
const anchors = new Set(['center', 'left', 'right', 'top', 'bottom', 'top-left', 'top-right', 'bottom-left', 'bottom-right']);
/**
* Utility class to assist managing values for text-variable-anchor-offset property. Create instances from
* bare arrays using the static method `VariableAnchorOffsetCollection.parse`.
* @private
*/
class VariableAnchorOffsetCollection {
constructor(values) {
this.values = values.slice();
}
static parse(input) {
if (input instanceof VariableAnchorOffsetCollection) {
return input;
}
if (!Array.isArray(input) ||
input.length < 1 ||
input.length % 2 !== 0) {
return undefined;
}
for (let i = 0; i < input.length; i += 2) {
// Elements in even positions should be anchor positions; Elements in odd positions should be offset values
const anchorValue = input[i];
const offsetValue = input[i + 1];
if (typeof anchorValue !== 'string' || !anchors.has(anchorValue)) {
return undefined;
}
if (!Array.isArray(offsetValue) || offsetValue.length !== 2 || typeof offsetValue[0] !== 'number' || typeof offsetValue[1] !== 'number') {
return undefined;
}
}
return new VariableAnchorOffsetCollection(input);
}
toString() {
return JSON.stringify(this.values);
}
static interpolate(from, to, t) {
const fromValues = from.values;
const toValues = to.values;
if (fromValues.length !== toValues.length) {
throw new RuntimeError(`Cannot interpolate values of different length. from: ${from.toString()}, to: ${to.toString()}`);
}
const output = [];
for (let i = 0; i < fromValues.length; i += 2) {
// Anchor entries must match
if (fromValues[i] !== toValues[i]) {
throw new RuntimeError(`Cannot interpolate values containing mismatched anchors. from[${i}]: ${fromValues[i]}, to[${i}]: ${toValues[i]}`);
}
output.push(fromValues[i]);
// Interpolate the offset values for each anchor
const [fx, fy] = fromValues[i + 1];
const [tx, ty] = toValues[i + 1];
output.push([interpolateNumber(fx, tx, t), interpolateNumber(fy, ty, t)]);
}
return new VariableAnchorOffsetCollection(output);
}
}
class ResolvedImage {
constructor(options) {
this.name = options.name;
this.available = options.available;
}
toString() {
return this.name;
}
static fromString(name) {
if (!name)
return null; // treat empty values as no image
return new ResolvedImage({ name, available: false });
}
}
class ProjectionDefinition {
constructor(from, to, transition) {
this.from = from;
this.to = to;
this.transition = transition;
}
static interpolate(from, to, t) {
return new ProjectionDefinition(from, to, t);
}
static parse(input) {
if (input instanceof ProjectionDefinition) {
return input;
}
if (Array.isArray(input) && input.length === 3 && typeof input[0] === 'string' && typeof input[1] === 'string' && typeof input[2] === 'number') {
return new ProjectionDefinition(input[0], input[1], input[2]);
}
if (typeof input === 'object' && typeof input.from === 'string' && typeof input.to === 'string' && typeof input.transition === 'number') {
return new ProjectionDefinition(input.from, input.to, input.transition);
}
if (typeof input === 'string') {
return new ProjectionDefinition(input, input, 1);
}
return undefined;
}
}
function validateRGBA(r, g, b, a) {
if (!(typeof r === 'number' && r >= 0 && r <= 255 &&
typeof g === 'number' && g >= 0 && g <= 255 &&
typeof b === 'number' && b >= 0 && b <= 255)) {
const value = typeof a === 'number' ? [r, g, b, a] : [r, g, b];
return `Invalid rgba value [${value.join(', ')}]: 'r', 'g', and 'b' must be between 0 and 255.`;
}
if (!(typeof a === 'undefined' || (typeof a === 'number' && a >= 0 && a <= 1))) {
return `Invalid rgba value [${[r, g, b, a].join(', ')}]: 'a' must be between 0 and 1.`;
}
return null;
}
function isValue(mixed) {
if (mixed === null ||
typeof mixed === 'string' ||
typeof mixed === 'boolean' ||
typeof mixed === 'number' ||
mixed instanceof ProjectionDefinition ||
mixed instanceof Color ||
mixed instanceof Collator ||
mixed instanceof Formatted ||
mixed instanceof Padding ||
mixed instanceof NumberArray ||
mixed instanceof ColorArray ||
mixed instanceof VariableAnchorOffsetCollection ||
mixed instanceof ResolvedImage) {
return true;
}
else if (Array.isArray(mixed)) {
for (const item of mixed) {
if (!isValue(item)) {
return false;
}
}
return true;
}
else if (typeof mixed === 'object') {
for (const key in mixed) {
if (!isValue(mixed[key])) {
return false;
}
}
return true;
}
else {
return false;
}
}
function typeOf(value) {
if (value === null) {
return NullType;
}
else if (typeof value === 'string') {
return StringType;
}
else if (typeof value === 'boolean') {
return BooleanType;
}
else if (typeof value === 'number') {
return NumberType;
}
else if (value instanceof Color) {
return ColorType;
}
else if (value instanceof ProjectionDefinition) {
return ProjectionDefinitionType;
}
else if (value instanceof Collator) {
return CollatorType;
}
else if (value instanceof Formatted) {
return FormattedType;
}
else if (value instanceof Padding) {
return PaddingType;
}
else if (value instanceof NumberArray) {
return NumberArrayType;
}
else if (value instanceof ColorArray) {
return ColorArrayType;
}
else if (value instanceof VariableAnchorOffsetCollection) {
return VariableAnchorOffsetCollectionType;
}
else if (value instanceof ResolvedImage) {
return ResolvedImageType;
}
else if (Array.isArray(value)) {
const length = value.length;
let itemType;
for (const item of value) {
const t = typeOf(item);
if (!itemType) {
itemType = t;
}
else if (itemType === t) {
continue;
}
else {
itemType = ValueType;
break;
}
}
return array(itemType || ValueType, length);
}
else {
return ObjectType;
}
}
function valueToString(value) {
const type = typeof value;
if (value === null) {
return '';
}
else if (type === 'string' || type === 'number' || type === 'boolean') {
return String(value);
}
else if (value instanceof Color || value instanceof ProjectionDefinition || value instanceof Formatted || value instanceof Padding || value instanceof NumberArray || value instanceof ColorArray || value instanceof VariableAnchorOffsetCollection || value instanceof ResolvedImage) {
return value.toString();
}
else {
return JSON.stringify(value);
}
}
class Literal {
constructor(type, value) {
this.type = type;
this.value = value;
}
static parse(args, context) {
if (args.length !== 2)
return context.error(`'literal' expression requires exactly one argument, but found ${args.length - 1} instead.`);
if (!isValue(args[1]))
return context.error('invalid value');
const value = args[1];
let type = typeOf(value);
// special case: infer the item type if possible for zero-length arrays
const expected = context.expectedType;
if (type.kind === 'array' &&
type.N === 0 &&
expected &&
expected.kind === 'array' &&
(typeof expected.N !== 'number' || expected.N === 0)) {
type = expected;
}
return new Literal(type, value);
}
evaluate() {
return this.value;
}
eachChild() { }
outputDefined() {
return true;
}
}
const types$1 = {
string: StringType,
number: NumberType,
boolean: BooleanType,
object: ObjectType
};
class Assertion {
constructor(type, args) {
this.type = type;
this.args = args;
}
static parse(args, context) {
if (args.length < 2)
return context.error('Expected at least one argument.');
let i = 1;
let type;
const name = args[0];
if (name === 'array') {
let itemType;
if (args.length > 2) {
const type = args[1];
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);
itemType = types$1[type];
i++;
}
else {
itemType = ValueType;
}
let N;
if (args.length > 3) {
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);
}
N = args[2];
i++;
}
type = array(itemType, N);
}
else {
if (!types$1[name])
throw new Error(`Types doesn't contain name = ${name}`);
type = types$1[name];
}
const parsed = [];
for (; i < args.length; i++) {
const input = context.parse(args[i], i, ValueType);
if (!input)
return null;
parsed.push(input);
}
return new Assertion(type, parsed);
}
evaluate(ctx) {
for (let i = 0; i < this.args.length; i++) {
const value = this.args[i].evaluate(ctx);
const error = checkSubtype(this.type, typeOf(value));
if (!error) {
return value;
}
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.`);
}
}
throw new Error();
}
eachChild(fn) {
this.args.forEach(fn);
}
outputDefined() {
return this.args.every(arg => arg.outputDefined());
}
}
const types = {
'to-boolean': BooleanType,
'to-color': ColorType,
'to-number': NumberType,
'to-string': StringType
};
/**
* Special form for error-coalescing coercion expressions "to-number",
* "to-color". Since these coercions can fail at runtime, they accept multiple
* arguments, only evaluating one at a time until one succeeds.
*
* @private
*/
class Coercion {
constructor(type, args) {
this.type = type;
this.args = args;
}
static parse(args, context) {
if (args.length < 2)
return context.error('Expected at least one argument.');
const name = args[0];
if (!types[name])
throw new Error(`Can't parse ${name} as it is not part of the known types`);
if ((name === 'to-boolean' || name === 'to-string') && args.length !== 2)
return context.error('Expected one argument.');
const type = types[name];
const parsed = [];
for (let i = 1; i < args.length; i++) {
const input = context.parse(args[i], i, ValueType);
if (!input)
return null;
parsed.push(input);
}
return new Coercion(type, parsed);
}
evaluate(ctx) {
switch (this.type.kind) {
case 'boolean':
return Boolean(this.args[0].evaluate(ctx));
case 'color': {
let input;
let error;
for (const arg of this.args) {
input = arg.evaluate(ctx);
error = null;
if (input instanceof Color) {
return input;
}
else if (typeof input === 'string') {
const c = ctx.parseColor(input);
if (c)
return c;
}
else if (Array.isArray(input)) {
if (input.length < 3 || input.length > 4) {
error = `Invalid rgba value ${JSON.stringify(input)}: expected an array containing either three or four numeric values.`;
}
else {
error = validateRGBA(input[0], input[1], input[2], input[3]);
}
if (!error) {
return new Color(input[0] / 255, input[1] / 255, input[2] / 255, input[3]);
}
}
}
throw new RuntimeError(error || `Could not parse color from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`);
}
case 'padding': {
let input;
for (const arg of this.args) {
input = arg.evaluate(ctx);
const pad = Padding.parse(input);
if (pad) {
return pad;
}
}
throw new RuntimeError(`Could not parse padding from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`);
}
case 'numberArray': {
let input;
for (const arg of this.args) {
input = arg.evaluate(ctx);
const val = NumberArray.parse(input);
if (val) {
return val;
}
}
throw new RuntimeError(`Could not parse numberArray from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`);
}
case 'colorArray': {
let input;
for (const arg of this.args) {
input = arg.evaluate(ctx);
const val = ColorArray.parse(input);
if (val) {
return val;
}
}
throw new RuntimeError(`Could not parse colorArray from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`);
}
case 'variableAnchorOffsetCollection': {
let input;
for (const arg of this.args) {
input = arg.evaluate(ctx);
const coll = VariableAnchorOffsetCollection.parse(input);
if (coll) {
return coll;
}
}
throw new RuntimeError(`Could not parse variableAnchorOffsetCollection from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`);
}
case 'number': {
let value = null;
for (const arg of this.args) {
value = arg.evaluate(ctx);
if (value === null)
return 0;
const num = Number(value);
if (isNaN(num))
continue;
return num;
}
throw new RuntimeError(`Could not convert ${JSON.stringify(value)} to number.`);
}
case 'formatted':
// There is no explicit 'to-formatted' but this coercion can be implicitly
// created by properties that expect the 'formatted' type.
return Formatted.fromString(valueToString(this.args[0].evaluate(ctx)));
case 'resolvedImage':
return ResolvedImage.fromString(valueToString(this.args[0].evaluate(ctx)));
case 'projectionDefinition':
return this.args[0].evaluate(ctx);
default:
return valueToString(this.args[0].evaluate(ctx));
}
}
eachChild(fn) {
this.args.forEach(fn);
}
outputDefined() {
return this.args.every(arg => arg.outputDefined());
}
}
const geometryTypes = ['Unknown', 'Point', 'LineString', 'Polygon'];
class EvaluationContext {
constructor() {
this.globals = null;
this.feature = null;
this.featureState = null;
this.formattedSection = null;
this._parseColorCache = new Map();
this.availableImages = null;
this.canonical = null;
}
id() {
return this.feature && 'id' in this.feature ? this.feature.id : null;
}
geometryType() {
return this.feature ? typeof this.feature.type === 'number' ? geometryTypes[this.feature.type] : this.feature.type : null;
}
geometry() {
return this.feature && 'geometry' in this.feature ? this.feature.geometry : null;
}
canonicalID() {
return this.canonical;
}
properties() {
return this.feature && this.feature.properties || {};
}
parseColor(input) {
let cached = this._parseColorCache.get(input);
if (!cached) {
cached = Color.parse(input);
this._parseColorCache.set(input, cached);
}
return cached;
}
}
/**
* State associated parsing at a given point in an expression tree.
* @private
*/
class ParsingContext {
constructor(registry, isConstantFunc, path = [], expectedType, scope = new Scope(), errors = []) {
this.registry = registry;
this.path = path;
this.key = path.map(part => `[${part}]`).join('');
this.scope = scope;
this.errors = errors;
this.expectedType = expectedType;
this._isConstant = isConstantFunc;
}
/**
* @param expr the JSON expression to parse
* @param index the optional argument index if this expression is an argument of a parent expression that's being parsed
* @param options
* @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.
* @private
*/
parse(expr, index, expectedType, bindings, options = {}) {
if (index) {
return this.concat(index, expectedType, bindings)._parse(expr, options);
}
return this._parse(expr, options);
}
_parse(expr, options) {
if (expr === null || typeof expr === 'string' || typeof expr === 'boolean' || typeof expr === 'number') {
expr = ['literal', expr];
}
function annotate(parsed, type, typeAnnotation) {
if (typeAnnotation === 'assert') {
return new Assertion(type, [parsed]);
}
else if (typeAnnotation === 'coerce') {
return new Coercion(type, [parsed]);
}
else {
return parsed;
}
}
if (Array.isArray(expr)) {
if (expr.length === 0) {
return this.error('Expected an array with at least one element. If you wanted a literal array, use ["literal", []].');
}
const op = expr[0];
if (typeof op !== 'string') {
this.error(`Expression name must be a string, but found ${typeof op} instead. If you wanted a literal array, use ["literal", [...]].`, 0);
return null;
}
const Expr = this.registry[op];
if (Expr) {
let parsed = Expr.parse(expr, this);
if (!parsed)
return null;
if (this.expectedType) {
const expected = this.expectedType;
const actual = parsed.type;
// When we expect a number, string, boolean, or array but have a value, wrap it in an assertion.
// When we expect a color or formatted string, but have a string or value, wrap it in a coercion.
// Otherwise, we do static type-checking.
//
// These behaviors are overridable for:
// * The "coalesce" operator, which needs to omit type annotations.
// * String-valued properties (e.g. `text-field`), where coercion is more convenient than assertion.
//
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');
}
else if (('projectionDefinition' === expected.kind && ['string', 'array'].includes(actual.kind)) ||
((['color', 'formatted', 'resolvedImage'].includes(expected.kind)) && ['value', 'string'].includes(actual.kind)) ||
((['padding', 'numberArray'].includes(expected.kind)) && ['value', 'number', 'array'].includes(actual.kind)) ||
('colorArray' === expected.kind && ['value', 'string', 'array'].includes(actual.kind)) ||
('variableAnchorOffsetCollection' === expected.kind && ['value', 'array'].includes(actual.kind))) {
parsed = annotate(parsed, expected, options.typeAnnotation || 'coerce');
}
else if (this.checkSubtype(expected, actual)) {
return null;
}
}
// If an expression's arguments are all literals, we can evaluate
// it immediately and replace it with a literal value in the
// parsed/compiled result. Expressions that expect an image should
// not be resolved here so we can later get the available images.
if (!(parsed instanceof Literal) && (parsed.type.kind !== 'resolvedImage') && this._isConstant(parsed)) {
const ec = new EvaluationContext();
try {
parsed = new Literal(parsed.type, parsed.evaluate(ec));
}
catch (e) {
this.error(e.message);
return null;
}
}
return parsed;
}
return this.error(`Unknown expression "${op}". If you wanted a literal array, use ["literal", [...]].`, 0);
}
else if (typeof expr === 'undefined') {
return this.error('\'undefined\' value invalid. Use null instead.');
}
else if (typeof expr === 'object') {
return this.error('Bare objects invalid. Use ["literal", {...}] instead.');
}
else {
return this.error(`Expected an array, but found ${typeof expr} instead.`);
}
}
/**
* Returns a copy of this context suitable for parsing the subexpression at
* index `index`, optionally appending to 'let' binding map.
*
* Note that `errors` property, intended for collecting errors while
* parsing, is copied by reference rather than cloned.
* @private
*/
concat(index, expectedType, bindings) {
const path = typeof index === 'number' ? this.path.concat(index) : this.path;
const scope = bindings ? this.scope.concat(bindings) : this.scope;
return new ParsingContext(this.registry, this._isConstant, path, expectedType || null, scope, this.errors);
}
/**
* Push a parsing (or type checking) error into the `this.errors`
* @param error The message
* @param keys Optionally specify the source of the error at a child
* of the current expression at `this.key`.
* @private
*/
error(error, ...keys) {
const key = `${this.key}${keys.map(k => `[${k}]`).join('')}`;
this.errors.push(new ExpressionParsingError(key, error));
}
/**
* Returns null if `t` is a subtype of `expected`; otherwise returns an
* error message and also pushes it to `this.errors`.
* @param expected The expected type
* @param t The actual type
* @returns null if `t` is a subtype of `expected`; otherwise returns an error message
*/
checkSubtype(expected, t) {
const error = checkSubtype(expected, t);
if (error)
this.error(error);
return error;
}
}
class Let {
constructor(bindings, result) {
this.type = result.type;
this.bindings = [].concat(bindings);
this.result = result;
}
evaluate(ctx) {
return this.result.evaluate(ctx);
}
eachChild(fn) {
for (const binding of this.bindings) {
fn(binding[1]);
}
fn(this.result);
}
static parse(args, context) {
if (args.length < 4)
return context.error(`Expected at least 3 arguments, but found ${args.length - 1} instead.`);
const bindings = [];
for (let i = 1; i < args.length - 1; i += 2) {
const name = args[i];
if (typeof name !== 'string') {
return context.error(`Expected string, but found ${typeof name} instead.`, i);
}
if (/[^a-zA-Z0-9_]/.test(name)) {
return context.error('Variable names must contain only alphanumeric characters or \'_\'.', i);
}
const value = context.parse(args[i + 1], i + 1);
if (!value)
return null;
bindings.push([name, value]);
}
const result = context.parse(args[args.length - 1], args.length - 1, context.expectedType, bindings);
if (!result)
return null;
return new Let(bindings, result);
}
outputDefined() {
return this.result.outputDefined();
}
}
class Var {
constructor(name, boundExpression) {
this.type = boundExpression.type;
this.name = name;
this.boundExpression = boundExpression;
}
static parse(args, context) {
if (args.length !== 2 || typeof args[1] !== 'string')
return context.error('\'var\' expression requires exactly one string literal argument.');
const name = args[1];
if (!context.scope.has(name)) {
return context.error(`Unknown variable "${name}". Make sure "${name}" has been bound in an enclosing "let" expression before using it.`, 1);
}
return new Var(name, context.scope.get(name));
}
evaluate(ctx) {
return this.boundExpression.evaluate(ctx);
}
eachChild() { }
outputDefined() {
return false;
}
}
class At {
constructor(type, index, input) {
this.type = type;
this.index = index;
this.input = input;
}
static parse(args, context) {
if (args.length !== 3)
return context.error(`Expected 2 arguments, but found ${args.length - 1} instead.`);
const index = context.parse(args[1], 1, NumberType);
const input = context.parse(args[2], 2, array(context.expectedType || ValueType));
if (!index || !input)
return null;
const t = input.type;
return new At(t.itemType, index, input);
}
evaluate(ctx) {
const index = this.index.evaluate(ctx);
const array = this.input.evaluate(ctx);
if (index < 0) {
throw new RuntimeError(`Array index out of bounds: ${index} < 0.`);
}
if (index >= array.length) {
throw new RuntimeError(`Array index out of bounds: ${index} > ${array.length - 1}.`);
}
if (index !== Math.floor(index)) {
throw new RuntimeError(`Array index must be an integer, but found ${index} instead.`);
}
return array[index];
}
eachChild(fn) {
fn(this.index);
fn(this.input);
}
outputDefined() {
return false;
}
}
class In {
constructor(needle, haystack) {
this.type = BooleanType;
this.needle = needle;
this.haystack = haystack;
}
static parse(args, context) {
if (args.length !== 3) {
return context.error(`Expected 2 arguments, but found ${args.length - 1} instead.`);
}
const needle = context.parse(args[1], 1, ValueType);
const haystack = context.parse(args[2], 2, ValueType);
if (!needle || !haystack)
return null;
if (!isValidType(needle.type, [BooleanType, StringType, NumberType, NullType, ValueType])) {
return context.error(`Expected first argument to be of type boolean, string, number or null, but found ${typeToString(needle.type)} instead`);
}
return new In(needle, haystack);
}
evaluate(ctx) {
const needle = this.needle.evaluate(ctx);
const haystack = this.haystack.evaluate(ctx);
if (!haystack)
return false;
if (!isValidNativeType(needle, ['boolean', 'string', 'number', 'null'])) {
throw new RuntimeError(`Expected first argument to be of type boolean, string, number or null, but found ${typeToString(typeOf(needle))} instead.`);
}
if (!isValidNativeType(haystack, ['string', 'array'])) {
throw new RuntimeError(`Expected second argument to be of type array or string, but found ${typeToString(typeOf(haystack))} instead.`);
}
return haystack.indexOf(needle) >= 0;
}
eachChild(fn) {
fn(this.needle);
fn(this.haystack);
}
outputDefined() {
return true;
}
}
class IndexOf {
constructor(needle, haystack, fromIndex) {
this.type = NumberType;
this.needle = needle;
this.haystack = haystack;
this.fromIndex = fromIndex;
}
static parse(args, context) {
if (args.length <= 2 || args.length >= 5) {
return context.error(`Expected 2 or 3 arguments, but found ${args.length - 1} instead.`);
}
const needle = context.parse(args[1], 1, ValueType);
const haystack = context.parse(args[2], 2, ValueType);
if (!needle || !haystack)
return null;
if (!isValidType(needle.type, [BooleanType, StringType, NumberType, NullType, ValueType])) {
return context.error(`Expected first argument to be of type boolean, string, number or null, but found ${typeToString(needle.type)} instead`);
}
if (args.length === 4) {
const fromIndex = context.parse(args[3], 3, NumberType);
if (!fromIndex)
return null;
return new IndexOf(needle, haystack, fromIndex);
}
else {
return new IndexOf(needle, haystack);
}
}
evaluate(ctx) {
const needle = this.needle.evaluate(ctx);
const haystack = this.haystack.evaluate(ctx);
if (!isValidNativeType(needle, ['boolean', 'string', 'number', 'null'])) {
throw new RuntimeError(`Expected first argument to be of type boolean, string, number or null, but found ${typeToString(typeOf(needle))} instead.`);
}
let fromIndex;
if (this.fromIndex) {
fromIndex = this.fromIndex.evaluate(ctx);
}
if (isValidNativeType(haystack, ['string'])) {
const rawIndex = haystack.indexOf(needle, fromIndex);
if (rawIndex === -1) {
return -1;
}
else {
// The index may be affected by surrogate pairs, so get the length of the preceding substring.
return [...haystack.slice(0, rawIndex)].length;
}
}
else if (isValidNativeType(haystack, ['array'])) {
return haystack.indexOf(needle, fromIndex);
}
else {
throw new RuntimeError(`Expected second argument to be of type array or string, but found ${typeToString(typeOf(haystack))} instead.`);
}
}
eachChild(fn) {
fn(this.needle);
fn(this.haystack);
if (this.fromIndex) {
fn(this.fromIndex);
}
}
outputDefined() {
return false;
}
}
class Match {
constructor(inputType, outputType, input, cases, outputs, otherwise) {
this.inputType = inputType;
this.type = outputType;
this.input = input;
this.cases = cases;
this.outputs = outputs;
this.otherwise = otherwise;
}
static parse(args, context) {
if (args.length < 5)
return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`);
if (args.length % 2 !== 1)
return context.error('Expected an even number of arguments.');
let inputType;
let outputType;
if (context.expectedType && context.expectedType.kind !== 'value') {
outputType = context.expectedType;
}
const cases = {};
const outputs = [];
for (let i = 2; i < args.length - 1; i += 2) {
let labels = args[i];
const value = args[i + 1];
if (!Array.isArray(labels)) {
labels = [labels];
}
const labelContext = context.concat(i);
if (labels.length === 0) {
return labelContext.error('Expected at least one branch label.');
}
for (const label of labels) {
if (typeof label !== 'number' && typeof label !== 'string') {
return labelContext.error('Branch labels must be numbers or strings.');
}
else if (typeof label === 'number' && Math.abs(label) > Number.MAX_SAFE_INTEGER) {
return labelContext.error(`Branch labels must be integers no larger than ${Number.MAX_SAFE_INTEGER}.`);
}
else if (typeof label === 'number' && Math.floor(label) !== label) {
return labelContext.error('Numeric branch labels must be integer values.');
}
else if (!inputType) {
inputType = typeOf(label);
}
else if (labelContext.checkSubtype(inputType, typeOf(label))) {
return null;
}
if (typeof cases[String(label)] !== 'undefined') {
return labelContext.error('Branch labels must be unique.');
}
cases[String(label)] = outputs.length;
}
const result = context.parse(value, i, outputType);
if (!result)
return null;
outputType = outputType || result.type;
outputs.push(result);
}
const input = context.parse(args[1], 1, ValueType);
if (!input)
return null;
const otherwise = context.parse(args[args.length - 1], args.length - 1, outputType);
if (!otherwise)
return null;
if (input.type.kind !== 'value' && context.concat(1).checkSubtype(inputType, input.type)) {
return null;
}
return new Match(inputType, outputType, input, cases, outputs, otherwise);
}
evaluate(ctx) {
const input = this.input.evaluate(ctx);
const output = (typeOf(input) === this.inputType && this.outputs[this.cases[input]]) || this.otherwise;
return output.evaluate(ctx);
}
eachChild(fn) {
fn(this.input);
this.outputs.forEach(fn);
fn(this.otherwise);
}
outputDefined() {
return this.outputs.every(out => out.outputDefined()) && this.otherwise.outputDefined();
}
}
class Case {
constructor(type, branches, otherwise) {
this.type = type;
this.branches = branches;
this.otherwise = otherwise;
}
static parse(args, context) {
if (args.length < 4)
return context.error(`Expected at least 3 arguments, but found only ${args.length - 1}.`);
if (args.length % 2 !== 0)
return context.error('Expected an odd number of arguments.');
let outputType;
if (context.expectedType && context.expectedType.kind !== 'value') {
outputType = context.expectedType;
}
const branches = [];
for (let i = 1; i < args.length - 1; i += 2) {
const test = context.parse(args[i], i, BooleanType);
if (!test)
return null;
const result = context.parse(args[i + 1], i + 1, outputType);
if (!result)
return null;
branches.push([test, result]);
outputType = outputType || result.type;
}
const otherwise = context.parse(args[args.length - 1], args.length - 1, outputType);
if (!otherwise)
return null;
if (!outputType)
throw new Error('Can\'t infer output type');
return new Case(outputType, branches, otherwise);
}
evaluate(ctx) {
for (const [test, expression] of this.branches) {
if (test.evaluate(ctx)) {
return expression.evaluate(ctx);
}
}
return this.otherwise.evaluate(ctx);
}
eachChild(fn) {
for (const [test, expression] of this.branches) {
fn(test);
fn(expression);
}
fn(this.otherwise);
}
outputDefined() {
return this.branches.every(([_, out]) => out.outputDefined()) && this.otherwise.outputDefined();
}
}
class Slice {
constructor(type, input, beginIndex, endIndex) {
this.type = type;
this.input = input;
this.beginIndex = beginIndex;
this.endIndex = endIndex;
}
static parse(args, context) {
if (args.length <= 2 || args.length >= 5) {
return context.error(`Expected 2 or 3 arguments, but found ${args.length - 1} instead.`);
}
const input = context.parse(args[1], 1, ValueType);
const beginIndex = context.parse(args[2], 2, NumberType);
if (!input || !beginIndex)
return null;
if (!isValidType(input.type, [array(ValueType), StringType, ValueType])) {
return context.error(`Expected first argument to be of type array or string, but found ${typeToString(input.type)} instead`);
}
if (args.length === 4) {
const endIndex = context.parse(args[3], 3, NumberType);
if (!endIndex)
return null;
return new Slice(input.type, input, beginIndex, endIndex);
}
else {
return new Slice(input.type, input, beginIndex);
}
}
evaluate(ctx) {
const input = this.input.evaluate(ctx);
const beginIndex = this.beginIndex.evaluate(ctx);
let endIndex;
if (this.endIndex) {
endIndex = this.endIndex.evaluate(ctx);
}
if (isValidNativeType(input, ['string'])) {
// Indices may be affected by surrogate pairs.
return [...input].slice(beginIndex, endIndex).join('');
}
else if (isValidNativeType(input, ['array'])) {
return input.slice(beginIndex, endIndex);
}
else {
throw new RuntimeError(`Expected first argument to be of type array or string, but found ${typeToString(typeOf(input))} instead.`);
}
}
eachChild(fn) {
fn(this.input);
fn(this.beginIndex);
if (this.endIndex) {
fn(this.endIndex);
}
}
outputDefined() {
return false;
}
}
/**
* Returns the index of the last stop <= input, or 0 if it doesn't exist.
* @private
*/
function findStopLessThanOrEqualTo(stops, input) {
const lastIndex = stops.length - 1;
let lowerIndex = 0;
let upperIndex = lastIndex;
let currentIndex = 0;
let currentValue, nextValue;
while (lowerIndex <= upperIndex) {
currentIndex = Math.floor((lowerIndex + upperIndex) / 2);
currentValue = stops[currentIndex];
nextValue = stops[currentIndex + 1];
if (currentValue <= input) {
if (currentIndex === lastIndex || input < nextValue) { // Search complete
return currentIndex;
}
lowerIndex = currentIndex + 1;
}
else if (currentValue > input) {
upperIndex = currentIndex - 1;
}
else {
throw new RuntimeError('Input is not a number.');
}
}
return 0;
}
class Step {
constructor(type, input, stops) {
this.type = type;
this.input = input;
this.labels = [];
this.outputs = [];
for (const [label, expression] of stops) {
this.labels.push(label);
this.outputs.push(expression);
}
}
static parse(args, context) {
if (args.length - 1 < 4) {
return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`);
}
if ((args.length - 1) % 2 !== 0) {
return context.error('Expected an even number of arguments.');
}
const input = context.parse(args[1], 1, NumberType);
if (!input)
return null;
const stops = [];
let outputType = null;
if (context.expectedType && context.expectedType.kind !== 'value') {
outputType = context.expectedType;
}
for (let i = 1; i < args.length; i += 2) {
const label = i === 1 ? -Infinity : args[i];
const value = args[i + 1];
const labelKey = i;
const valueKey = i + 1;
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);
}
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);
}
const parsed = context.parse(value, valueKey, outputType);
if (!parsed)
return null;
outputType = outputType || parsed.type;
stops.push([label, parsed]);
}
return new Step(outputType, input, stops);
}
evaluate(ctx) {
const labels = this.labels;
const outputs = this.outputs;
if (labels.length === 1) {
return outputs[0].evaluate(ctx);
}
const value = this.input.evaluate(ctx);
if (value <= labels[0]) {
return outputs[0].evaluate(ctx);
}
const stopCount = labels.length;
if (value >= labels[stopCount - 1]) {
return outputs[stopCount - 1].evaluate(ctx);
}
const index = findStopLessThanOrEqualTo(labels, value);
return outputs[index].evaluate(ctx);
}
eachChild(fn) {
fn(this.input);
for (const expression of this.outputs) {
fn(expression);
}
}
outputDefined() {
return this.outputs.every(out => out.outputDefined());
}
}
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
var unitbezier;
var hasRequiredUnitbezier;
function requireUnitbezier () {
if (hasRequiredUnitbezier) return unitbezier;
hasRequiredUnitbezier = 1;
unitbezier = UnitBezier;
function UnitBezier(p1x, p1y, p2x, p2y) {
// Calculate the polynomial coefficients, implicit first and last control points are (0,0) and (1,1).
this.cx = 3.0 * p1x;
this.bx = 3.0 * (p2x - p1x) - this.cx;
this.ax = 1.0 - this.cx - this.bx;
this.cy = 3.0 * p1y;
this.by = 3.0 * (p2y - p1y) - this.cy;
this.ay = 1.0 - this.cy - this.by;
this.p1x = p1x;
this.p1y = p1y;
this.p2x = p2x;
this.p2y = p2y;
}
UnitBezier.prototype = {
sampleCurveX: function (t) {
// `ax t^3 + bx t^2 + cx t' expanded using Horner's rule.
return ((this.ax * t + this.bx) * t + this.cx) * t;
},
sampleCurveY: function (t) {
return ((this.ay * t + this.by) * t + this.cy) * t;
},
sampleCurveDerivativeX: function (t) {
return (3.0 * this.ax * t + 2.0 * this.bx) * t + this.cx;
},
solveCurveX: function (x, epsilon) {
if (epsilon === undefined) epsilon = 1e-6;
if (x < 0.0) return 0.0;
if (x > 1.0) return 1.0;
var t = x;
// First try a few iterations of Newton's method - normally very fast.
for (var i = 0; i < 8; i++) {
var x2 = this.sampleCurveX(t) - x;
if (Math.abs(x2) < epsilon) return t;
var d2 = this.sampleCurveDerivativeX(t);
if (Math.abs(d2) < 1e-6) break;
t = t - x2 / d2;
}
// Fall back to the bisection method for reliability.
var t0 = 0.0;
var t1 = 1.0;
t = x;
for (i = 0; i < 20; i++) {
x2 = this.sampleCurveX(t);
if (Math.abs(x2 - x) < epsilon) break;
if (x > x2) {
t0 = t;
} else {
t1 = t;
}
t = (t1 - t0) * 0.5 + t0;
}
return t;
},
solve: function (x, epsilon) {
return this.sampleCurveY(this.solveCurveX(x, epsilon));
}
};
return unitbezier;
}
var unitbezierExports = requireUnitbezier();
var UnitBezier = /*@__PURE__*/getDefaultExportFromCjs(unitbezierExports);
class Interpolate {
constructor(type, operator, interpolation, input, stops) {
this.type = type;
this.operator = operator;
this.interpolation = interpolation;
this.input = input;
this.labels = [];
this.outputs = [];
for (const [label, expression] of stops) {
this.labels.push(label);
this.outputs.push(expression);
}
}
static interpolationFactor(interpolation, input, lower, upper) {
let t = 0;
if (interpolation.name === 'exponential') {
t = exponentialInterpolation(input, interpolation.base, lower, upper);
}
else if (interpolation.name === 'linear') {
t = exponentialInterpolation(input, 1, lower, upper);
}
else if (interpolation.name === 'cubic-bezier') {
const c = interpolation.controlPoints;
const ub = new UnitBezier(c[0], c[1], c[2], c[3]);
t = ub.solve(exponentialInterpolation(input, 1, lower, upper));
}
return t;
}
static parse(args, context) {
let [operator, interpolation, input, ...rest] = args;
if (!Array.isArray(interpolation) || interpolation.length === 0) {
return context.error('Expected an interpolation type expression.', 1);
}
if (interpolation[0] === 'linear') {
interpolation = { name: 'linear' };
}
else if (interpolation[0] === 'exponential') {
const base = interpolation[1];
if (typeof base !== 'number')
return context.error('Exponential interpolation requires a numeric base.', 1, 1);
interpolation = {
name: 'exponential',
base
};
}
else if (interpolation[0] === 'cubic-bezier') {
const controlPoints = interpolation.slice(1);
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);
}
interpolation = {
name: 'cubic-bezier',
controlPoints: controlPoints
};
}
else {
return context.error(`Unknown interpolation type ${String(interpolation[0])}`, 1, 0);
}
if (args.length - 1 < 4) {
return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`);
}
if ((args.length - 1) % 2 !== 0) {
return context.error('Expected an even number of arguments.');
}
input = context.parse(input, 2, NumberType);
if (!input)
return null;
const stops = [];
let outputType = null;
if ((operator === 'interpolate-hcl' || operator === 'interpolate-lab') && context.expectedType != ColorArrayType) {
outputType = ColorType;
}
else if (context.expectedType && context.expectedType.kind !== 'value') {
outputType = context.expectedType;
}
for (let i = 0; i < rest.length; i += 2) {
const label = rest[i];
const value = rest[i + 1];
const labelKey = i + 3;
const valueKey = i + 4;
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);
}
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);
}
const parsed = context.parse(value, valueKey, outputType);
if (!parsed)
return null;
outputType = outputType || parsed.type;
stops.push([label, parsed]);
}
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.`);
}
return new Interpolate(outputType, operator, interpolation, input, stops);
}
evaluate(ctx) {
const labels = this.labels;
const outputs = this.outputs;
if (labels.length === 1) {
return outputs[0].evaluate(ctx);
}
const value = this.input.evaluate(ctx);
if (value <= labels[0]) {
return outputs[0].evaluate(ctx);
}
const stopCount = labels.length;
if (value >= labels[stopCount - 1]) {
return outputs[stopCount - 1].evaluate(ctx);
}
const index = findStopLessThanOrEqualTo(labels, value);
const lower = labels[index];
const upper = labels[index + 1];
const t = Interpolate.interpolationFactor(this.interpolation, value, lower, upper);
const outputLower = outputs[index].evaluate(ctx);
const outputUpper = outputs[index + 1].evaluate(ctx);
switch (this.operator) {
case 'interpolate':
switch (this.type.kind) {
case 'number':
return interpolateNumber(outputLower, outputUpper, t);
case 'color':
return Color.interpolate(outputLower, outputUpper, t);
case 'padding':
return Padding.interpolate(outputLower, outputUpper, t);
case 'colorArray':
return ColorArray.interpolate(outputLower, outputUpper, t);
case 'numberArray':
return NumberArray.interpolate(outputLower, outputUpper, t);
case 'variableAnchorOffsetCollection':
return VariableAnchorOffsetCollection.interpolate(outputLower, outputUpper, t);
case 'array':
return interpolateArray(outputLower, outputUpper, t);
case 'projectionDefinition':
return ProjectionDefinition.interpolate(outputLower, outputUpper, t);
}
case 'interpolate-hcl':
switch (this.type.kind) {
case 'color':
return Color.interpolate(outputLower, outputUpper, t, 'hcl');
case 'colorArray':
return ColorArray.interpolate(outputLower, outputUpper, t, 'hcl');
}
case 'interpolate-lab':
switch (this.type.kind) {
case 'color':
return Color.interpolate(outputLower, outputUpper, t, 'lab');
case 'colorArray':
return ColorArray.interpolate(outputLower, outputUpper, t, 'lab');
}
}
}
eachChild(fn) {
fn(this.input);
for (const expression of this.outputs) {
fn(expression);
}
}
outputDefined() {
return this.outputs.every(out => out.outputDefined());
}
}
/**
* Returns a ratio that can be used to interpolate between exponential function
* stops.
* 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,
* and `a` and `b` are constants affording sufficient degrees of freedom to fit
* the function to the given stops.
*
* Here's a bit of algebra that lets us compute `f(x)` directly from the stop
* values without explicitly solving for `a` and `b`:
*
* First stop value: `f(x0) = y0 = a * base^x0 + b`
* Second stop value: `f(x1) = y1 = a * base^x1 + b`
* => `y1 - y0 = a(base^x1 - base^x0)`
* => `a = (y1 - y0)/(base^x1 - base^x0)`
*
* Desired value: `f(x) = y = a * base^x + b`
* => `f(x) = y0 + a * (base^x - base^x0)`
*
* From the above, we can replace the `a` in `a * (base^x - base^x0)` and do a
* little algebra:
* ```
* a * (base^x - base^x0) = (y1 - y0)/(base^x1 - base^x0) * (base^x - base^x0)
* = (y1 - y0) * (base^x - base^x0) / (base^x1 - base^x0)
* ```
*
* If we let `(base^x - base^x0) / (base^x1 base^x0)`, then we have
* `f(x) = y0 + (y1 - y0) * ratio`. In other words, `ratio` may be treated as
* an interpolation factor between the two stops' output values.
*
* (Note: a slightly different form for `ratio`,
* `(base^(x-x0) - 1) / (base^(x1-x0) - 1) `, is equivalent, but requires fewer
* expensive `Math.pow()` operations.)
*
* @private
*/
function exponentialInterpolation(input, base, lowerValue, upperValue) {
const difference = upperValue - lowerValue;
const progress = input - lowerValue;
if (difference === 0) {
return 0;
}
else if (base === 1) {
return progress / difference;
}
else {
return (Math.pow(base, progress) - 1) / (Math.pow(base, difference) - 1);
}
}
const interpolateFactory = {
color: Color.interpolate,
number: interpolateNumber,
padding: Padding.interpolate,
numberArray: NumberArray.interpolate,
colorArray: ColorArray.interpolate,
variableAnchorOffsetCollection: VariableAnchorOffsetCollection.interpolate,
array: interpolateArray
};
class Coalesce {
constructor(type, args) {
this.type = type;
this.args = args;
}
static parse(args, context) {
if (args.length < 2) {
return context.error('Expected at least one argument.');
}
let outputType = null;
const expectedType = context.expectedType;
if (expectedType && expectedType.kind !== 'value') {
outputType = expectedType;
}
const parsedArgs = [];
for (const arg of args.slice(1)) {
const parsed = context.parse(arg, 1 + parsedArgs.length, outputType, undefined, { typeAnnotation: 'omit' });
if (!parsed)
return null;
outputType = outputType || parsed.type;
parsedArgs.push(parsed);
}
if (!outputType)
throw new Error('No output type');
// Above, we parse arguments without inferred type annotation so that
// they don't produce a runtime error for `null` input, which would
// preempt the desired null-coalescing behavior.
// Thus, if any of our arguments would have needed an annotation, we
// need to wrap the enclosing coalesce expression with it instead.
const needsAnnotation = expectedType &&
parsedArgs.some(arg => checkSubtype(expectedType, arg.type));
return needsAnnotation ?
new Coalesce(ValueType, parsedArgs) :
new Coalesce(outputType, parsedArgs);
}
evaluate(ctx) {
let result = null;
let argCount = 0;
let requestedImageName;
for (const arg of this.args) {
argCount++;
result = arg.evaluate(ctx);
// we need to keep track of the first requested image in a coalesce statement
// if coalesce can't find a valid image, we return the first image name so styleimagemissing can fire
if (result && result instanceof ResolvedImage && !result.available) {
if (!requestedImageName) {
requestedImageName = result.name;
}
result = null;
if (argCount === this.args.length) {
result = requestedImageName;
}
}
if (result !== null)
break;
}
return result;
}
eachChild(fn) {
this.args.forEach(fn);
}
outputDefined() {
return this.args.every(arg => arg.outputDefined());
}
}
function isComparableType(op, type) {
if (op === '==' || op === '!=') {
// equality operator
return type.kind === 'boolean' ||
type.kind === 'string' ||
type.kind === 'number' ||
type.kind === 'null' ||
type.kind === 'value';
}
else {
// ordering operator
return type.kind === 'string' ||
type.kind === 'number' ||
type.kind === 'value';
}
}
function eq(ctx, a, b) { return a === b; }
function neq(ctx, a, b) { return a !== b; }
function lt(ctx, a, b) { return a < b; }
function gt(ctx, a, b) { return a > b; }
function lteq(ctx, a, b) { return a <= b; }
function gteq(ctx, a, b) { return a >= b; }
function eqCollate(ctx, a, b, c) { return c.compare(a, b) === 0; }
function neqCollate(ctx, a, b, c) { return !eqCollate(ctx, a, b, c); }
function ltCollate(ctx, a, b, c) { return c.compare(a, b) < 0; }
function gtCollate(ctx, a, b, c) { return c.compare(a, b) > 0; }
function lteqCollate(ctx, a, b, c) { return c.compare(a, b) <= 0; }
function gteqCollate(ctx, a, b, c) { return c.compare(a, b) >= 0; }
/**
* Special form for comparison operators, implementing the signatures:
* - (T, T, ?Collator) => boolean
* - (T, value, ?Collator) => boolean
* - (value, T, ?Collator) => boolean
*
* For inequalities, T must be either value, string, or number. For ==/!=, it
* can also be boolean or null.
*
* Equality semantics are equivalent to Javascript's strict equality (===/!==)
* -- i.e., when the arguments' types don't match, == evaluates to false, != to
* true.
*
* When types don't match in an ordering comparison, a runtime error is thrown.
*
* @private
*/
function makeComparison(op, compareBasic, compareWithCollator) {
const isOrderComparison = op !== '==' && op !== '!=';
return class Comparison {
constructor(lhs, rhs, collator) {
this.type = BooleanType;
this.lhs = lhs;
this.rhs = rhs;
this.collator = collator;
this.hasUntypedArgument = lhs.type.kind === 'value' || rhs.type.kind === 'value';
}
static parse(args, context) {
if (args.length !== 3 && args.length !== 4)
return context.error('Expected two or three arguments.');
const op = args[0];
let lhs = context.parse(args[1], 1, ValueType);
if (!lhs)
return null;
if (!isComparableType(op, lhs.type)) {
return context.concat(1).error(`"${op}" comparisons are not supported for type '${typeToString(lhs.type)}'.`);
}
let rhs = context.parse(args[2], 2, ValueType);
if (!rhs)
return null;
if (!isComparableType(op, rhs.type)) {
return context.concat(2).error(`"${op}" comparisons are not supported for type '${typeToString(rhs.type)}'.`);
}
if (lhs.type.kind !== rhs.type.kind &&
lhs.type.kind !== 'value' &&
rhs.type.kind !== 'value') {
return context.error(`Cannot compare types '${typeToString(lhs.type)}' and '${typeToString(rhs.type)}'.`);
}
if (isOrderComparison) {
// typing rules specific to less/greater than operators
if (lhs.type.kind === 'value' && rhs.type.kind !== 'value') {
// (value, T)
lhs = new Assertion(rhs.type, [lhs]);
}
else if (lhs.type.kind !== 'value' && rhs.type.kind === 'value') {
// (T, value)
rhs = new Assertion(lhs.type, [rhs]);
}
}
let collator = null;
if (args.length === 4) {
if (lhs.type.kind !== 'string' &&
rhs.type.kind !== 'string' &&
lhs.type.kind !== 'value' &&
rhs.type.kind !== 'value') {
return context.error('Cannot use collator to compare non-string types.');
}
collator = context.parse(args[3], 3, CollatorType);
if (!collator)
return null;
}
return new Comparison(lhs, rhs, collator);
}
evaluate(ctx) {
const lhs = this.lhs.evaluate(ctx);
const rhs = this.rhs.evaluate(ctx);
if (isOrderComparison && this.hasUntypedArgument) {
const lt = typeOf(lhs);
const rt = typeOf(rhs);
// check that type is string or number, and equal
if (lt.kind !== rt.kind || !(lt.kind === 'string' || lt.kind === 'number')) {
throw new RuntimeError(`Expected arguments for "${op}" to be (string, string) or (number, number), but found (${lt.kind}, ${rt.kind}) instead.`);
}
}
if (this.collator && !isOrderComparison && this.hasUntypedArgument) {
const lt = typeOf(lhs);
const rt = typeOf(rhs);
if (lt.kind !== 'string' || rt.kind !== 'string') {
return compareBasic(ctx, lhs, rhs);
}
}
return this.collator ?
compareWithCollator(ctx, lhs, rhs, this.collator.evaluate(ctx)) :
compareBasic(ctx, lhs, rhs);
}
eachChild(fn) {
fn(this.lhs);
fn(this.rhs);
if (this.collator) {
fn(this.collator);
}
}
outputDefined() {
return true;
}
};
}
const Equals = makeComparison('==', eq, eqCollate);
const NotEquals = makeComparison('!=', neq, neqCollate);
const LessThan = makeComparison('<', lt, ltCollate);
const GreaterThan = makeComparison('>', gt, gtCollate);
const LessThanOrEqual = makeComparison('<=', lteq, lteqCollate);
const GreaterThanOrEqual = makeComparison('>=', gteq, gteqCollate);
class CollatorExpression {
constructor(caseSensitive, diacriticSensitive, locale) {
this.type = CollatorType;
this.locale = locale;
this.caseSensitive = caseSensitive;
this.diacriticSensitive = diacriticSensitive;
}
static parse(args, context) {
if (args.length !== 2)
return context.error('Expected one argument.');
const options = args[1];
if (typeof options !== 'object' || Array.isArray(options))
return context.error('Collator options argument must be an object.');
const caseSensitive = context.parse(options['case-sensitive'] === undefined ? false : options['case-sensitive'], 1, BooleanType);
if (!caseSensitive)
return null;
const diacriticSensitive = context.parse(options['diacritic-sensitive'] === undefined ? false : options['diacritic-sensitive'], 1, BooleanType);
if (!diacriticSensitive)
return null;
let locale = null;
if (options['locale']) {
locale = context.parse(options['locale'], 1, StringType);
if (!locale)
return null;
}
return new CollatorExpression(caseSensitive, diacriticSensitive, locale);
}
evaluate(ctx) {
return new Collator(this.caseSensitive.evaluate(ctx), this.diacriticSensitive.evaluate(ctx), this.locale ? this.locale.evaluate(ctx) : null);
}
eachChild(fn) {
fn(this.caseSensitive);
fn(this.diacriticSensitive);
if (this.locale) {
fn(this.locale);
}
}
outputDefined() {
// Technically the set of possible outputs is the combinatoric set of Collators produced
// by all possible outputs of locale/caseSensitive/diacriticSensitive
// But for the primary use of Collators in comparison operators, we ignore the Collator's
// possible outputs anyway, so we can get away with leaving this false for now.
return false;
}
}
class NumberFormat {
constructor(number, locale, currency, minFractionDigits, maxFractionDigits) {
this.type = StringType;
this.number = number;
this.locale = locale;
this.currency = currency;
this.minFractionDigits = minFractionDigits;
this.maxFractionDigits = maxFractionDigits;
}
static parse(args, context) {
if (args.length !== 3)
return context.error('Expected two arguments.');
const number = context.parse(args[1], 1, NumberType);
if (!number)
return null;
const options = args[2];
if (typeof options !== 'object' || Array.isArray(options))
return context.error('NumberFormat options argument must be an object.');
let locale = null;
if (options['locale']) {
locale = context.parse(options['locale'], 1, StringType);
if (!locale)
return null;
}
let currency = null;
if (options['currency']) {
currency = context.parse(options['currency'], 1, StringType);
if (!currency)
return null;
}
let minFractionDigits = null;
if (options['min-fraction-digits']) {
minFractionDigits = context.parse(options['min-fraction-digits'], 1, NumberType);
if (!minFractionDigits)
return null;
}
let maxFractionDigits = null;
if (options['max-fraction-digits']) {
maxFractionDigits = context.parse(options['max-fraction-digits'], 1, NumberType);
if (!maxFractionDigits)
return null;
}
return new NumberFormat(number, locale, currency, minFractionDigits, maxFractionDigits);
}
evaluate(ctx) {
return new Intl.NumberFormat(this.locale ? this.locale.evaluate(ctx) : [], {
style: this.currency ? 'currency' : 'decimal',
currency: this.currency ? this.currency.evaluate(ctx) : undefined,
minimumFractionDigits: this.minFractionDigits ? this.minFractionDigits.evaluate(ctx) : undefined,
maximumFractionDigits: this.maxFractionDigits ? this.maxFractionDigits.evaluate(ctx) : undefined,
}).format(this.number.evaluate(ctx));
}
eachChild(fn) {
fn(this.number);
if (this.locale) {
fn(this.locale);
}
if (this.currency) {
fn(this.currency);
}
if (this.minFractionDigits) {
fn(this.minFractionDigits);
}
if (this.maxFractionDigits) {
fn(this.maxFractionDigits);
}
}
outputDefined() {
return false;
}
}
class FormatExpression {
constructor(sections) {
this.type = FormattedType;
this.sections = sections;
}
static parse(args, context) {
if (args.length < 2) {
return context.error('Expected at least one argument.');
}
const firstArg = args[1];
if (!Array.isArray(firstArg) && typeof firstArg === 'object') {
return context.error('First argument must be an image or text section.');
}
const sections = [];
let nextTokenMayBeObject = false;
for (let i = 1; i <= args.length - 1; ++i) {
const arg = args[i];
if (nextTokenMayBeObject && typeof arg === 'object' && !Array.isArray(arg)) {
nextTokenMayBeObject = false;
let scale = null;
if (arg['font-scale']) {
scale = context.parse(arg['font-scale'], 1, NumberType);
if (!scale)
return null;
}
let font = null;
if (arg['text-font']) {
font = context.parse(arg['text-font'], 1, array(StringType));
if (!font)
return null;
}
let textColor = null;
if (arg['text-color']) {
textColor = context.parse(arg['text-color'], 1, ColorType);
if (!textColor)
return null;
}
let verticalAlign = null;
if (arg['vertical-align']) {
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.`);
}
verticalAlign = context.parse(arg['vertical-align'], 1, StringType);
if (!verticalAlign)
return null;
}
const lastExpression = sections[sections.length - 1];
lastExpression.scale = scale;
lastExpression.font = font;
lastExpression.textColor = textColor;
lastExpression.verticalAlign = verticalAlign;
}
else {
const content = context.parse(args[i], 1, ValueType);
if (!content)
return null;
const kind = content.type.kind;
if (kind !== 'string' && kind !== 'value' && kind !== 'null' && kind !== 'resolvedImage')
return context.error('Formatted text type must be \'string\', \'value\', \'image\' or \'null\'.');
nextTokenMayBeObject = true;
sections.push({ content, scale: null, font: null, textColor: null, verticalAlign: null });
}
}
return new FormatExpression(sections);
}
evaluate(ctx) {
const evaluateSection = section => {
const evaluatedContent = section.content.evaluate(ctx);
if (typeOf(evaluatedContent) === ResolvedImageType) {
return new FormattedSection('', evaluatedContent, null, null, null, section.verticalAlign ? section.verticalAlign.evaluate(ctx) : null);
}
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);
};
return new Formatted(this.sections.map(evaluateSection));
}
eachChild(fn) {
for (const section of this.sections) {
fn(section.content);
if (section.scale) {
fn(section.scale);
}
if (section.font) {
fn(section.font);
}
if (section.textColor) {
fn(section.textColor);
}
if (section.verticalAlign) {
fn(section.verticalAlign);
}
}
}
outputDefined() {
// Technically the combinatoric set of all children
// Usually, this.text will be undefined anyway
return false;
}
}
class ImageExpression {
constructor(input) {
this.type = ResolvedImageType;
this.input = input;
}
static parse(args, context) {
if (args.length !== 2) {
return context.error('Expected two arguments.');
}
const name = context.parse(args[1], 1, StringType);
if (!name)
return context.error('No image name provided.');
return new ImageExpression(name);
}
evaluate(ctx) {
const evaluatedImageName = this.input.evaluate(ctx);
const value = ResolvedImage.fromString(evaluatedImageName);
if (value && ctx.availableImages)
value.available = ctx.availableImages.indexOf(evaluatedImageName) > -1;
return value;
}
eachChild(fn) {
fn(this.input);
}
outputDefined() {
// The output of image is determined by the list of available images in the evaluation context
return false;
}
}
class Length {
constructor(input) {
this.type = NumberType;
this.input = input;
}
static parse(args, context) {
if (args.length !== 2)
return context.error(`Expected 1 argument, but found ${args.length - 1} instead.`);
const input = context.parse(args[1], 1);
if (!input)
return null;
if (input.type.kind !== 'array' && input.type.kind !== 'string' && input.type.kind !== 'value')
return context.error(`Expected argument of type string or array, but found ${typeToString(input.type)} instead.`);
return new Length(input);
}
evaluate(ctx) {
const input = this.input.evaluate(ctx);
if (typeof input === 'string') {
// The length may be affected by surrogate pairs.
return [...input].length;
}
else if (Array.isArray(input)) {
return input.length;
}
else {
throw new RuntimeError(`Expected value to be of type string or array, but found ${typeToString(typeOf(input))} instead.`);
}
}
eachChild(fn) {
fn(this.input);
}
outputDefined() {
return false;
}
}
const EXTENT = 8192;
function getTileCoordinates(p, canonical) {
const x = mercatorXfromLng(p[0]);
const y = mercatorYfromLat(p[1]);
const tilesAtZoom = Math.pow(2, canonical.z);
return [Math.round(x * tilesAtZoom * EXTENT), Math.round(y * tilesAtZoom * EXTENT)];
}
function getLngLatFromTileCoord(coord, canonical) {
const tilesAtZoom = Math.pow(2, canonical.z);
const x = (coord[0] / EXTENT + canonical.x) / tilesAtZoom;
const y = (coord[1] / EXTENT + canonical.y) / tilesAtZoom;
return [lngFromMercatorXfromLng(x), latFromMercatorY(y)];
}
function mercatorXfromLng(lng) {
return (180 + lng) / 360;
}
function lngFromMercatorXfromLng(mercatorX) {
return mercatorX * 360 - 180;
}
function mercatorYfromLat(lat) {
return (180 - (180 / Math.PI * Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360)))) / 360;
}
function latFromMercatorY(mercatorY) {
return 360 / Math.PI * Math.atan(Math.exp((180 - mercatorY * 360) * Math.PI / 180)) - 90;
}
function updateBBox(bbox, coord) {
bbox[0] = Math.min(bbox[0], coord[0]);
bbox[1] = Math.min(bbox[1], coord[1]);
bbox[2] = Math.max(bbox[2], coord[0]);
bbox[3] = Math.max(bbox[3], coord[1]);
}
function boxWithinBox(bbox1, bbox2) {
if (bbox1[0] <= bbox2[0])
return false;
if (bbox1[2] >= bbox2[2])
return false;
if (bbox1[1] <= bbox2[1])
return false;
if (bbox1[3] >= bbox2[3])
return false;
return true;
}
function rayIntersect(p, p1, p2) {
return ((p1[1] > p[1]) !== (p2[1] > p[1])) && (p[0] < (p2[0] - p1[0]) * (p[1] - p1[1]) / (p2[1] - p1[1]) + p1[0]);
}
function pointOnBoundary(p, p1, p2) {
const x1 = p[0] - p1[0];
const y1 = p[1] - p1[1];
const x2 = p[0] - p2[0];
const y2 = p[1] - p2[1];
return (x1 * y2 - x2 * y1 === 0) && (x1 * x2 <= 0) && (y1 * y2 <= 0);
}
// a, b are end points for line segment1, c and d are end points for line segment2
function segmentIntersectSegment(a, b, c, d) {
// check if two segments are parallel or not
// precondition is end point a, b is inside polygon, if line a->b is
// parallel to polygon edge c->d, then a->b won't intersect with c->d
const vectorP = [b[0] - a[0], b[1] - a[1]];
const vectorQ = [d[0] - c[0], d[1] - c[1]];
if (perp(vectorQ, vectorP) === 0)
return false;
// If lines are intersecting with each other, the relative location should be:
// a and b lie in different sides of segment c->d
// c and d lie in different sides of segment a->b
if (twoSided(a, b, c, d) && twoSided(c, d, a, b))
return true;
return false;
}
function lineIntersectPolygon(p1, p2, polygon) {
for (const ring of polygon) {
// loop through every edge of the ring
for (let j = 0; j < ring.length - 1; ++j) {
if (segmentIntersectSegment(p1, p2, ring[j], ring[j + 1])) {
return true;
}
}
}
return false;
}
// ray casting algorithm for detecting if point is in polygon
function pointWithinPolygon(point, rings, trueIfOnBoundary = false) {
let inside = false;
for (const ring of rings) {
for (let j = 0; j < ring.length - 1; j++) {
if (pointOnBoundary(point, ring[j], ring[j + 1]))
return trueIfOnBoundary;
if (rayIntersect(point, ring[j], ring[j + 1]))
inside = !inside;
}
}
return inside;
}
function pointWithinPolygons(point, polygons) {
for (const polygon of polygons) {
if (pointWithinPolygon(point, polygon))
return true;
}
return false;
}
function lineStringWithinPolygon(line, polygon) {
// First, check if geometry points of line segments are all inside polygon
for (const point of line) {
if (!pointWithinPolygon(point, polygon)) {
return false;
}
}
// Second, check if there is line segment intersecting polygon edge
for (let i = 0; i < line.length - 1; ++i) {
if (lineIntersectPolygon(line[i], line[i + 1], polygon)) {
return false;
}
}
return true;
}
function lineStringWithinPolygons(line, polygons) {
for (const polygon of polygons) {
if (lineStringWithinPolygon(line, polygon))
return true;
}
return false;
}
function perp(v1, v2) {
return (v1[0] * v2[1] - v1[1] * v2[0]);
}
// check if p1 and p2 are in different sides of line segment q1->q2
function twoSided(p1, p2, q1, q2) {
// q1->p1 (x1, y1), q1->p2 (x2, y2), q1->q2 (x3, y3)
const x1 = p1[0] - q1[0];
const y1 = p1[1] - q1[1];
const x2 = p2[0] - q1[0];
const y2 = p2[1] - q1[1];
const x3 = q2[0] - q1[0];
const y3 = q2[1] - q1[1];
const det1 = (x1 * y3 - x3 * y1);
const det2 = (x2 * y3 - x3 * y2);
if ((det1 > 0 && det2 < 0) || (det1 < 0 && det2 > 0))
return true;
return false;
}
function getTilePolygon(coordinates, bbox, canonical) {
const polygon = [];
for (let i = 0; i < coordinates.length; i++) {
const ring = [];
for (let j = 0; j < coordinates[i].length; j++) {
const coord = getTileCoordinates(coordinates[i][j], canonical);
updateBBox(bbox, coord);
ring.push(coord);
}
polygon.push(ring);
}
return polygon;
}
function getTilePolygons(coordinates, bbox, canonical) {
const polygons = [];
for (let i = 0; i < coordinates.length; i++) {
const polygon = getTilePolygon(coordinates[i], bbox, canonical);
polygons.push(polygon);
}
return polygons;
}
function updatePoint(p, bbox, polyBBox, worldSize) {
if (p[0] < polyBBox[0] || p[0] > polyBBox[2]) {
const halfWorldSize = worldSize * 0.5;
let shift = (p[0] - polyBBox[0] > halfWorldSize) ? -worldSize : (polyBBox[0] - p[0] > halfWorldSize) ? worldSize : 0;
if (shift === 0) {
shift = (p[0] - polyBBox[2] > halfWorldSize) ? -worldSize : (polyBBox[2] - p[0] > halfWorldSize) ? worldSize : 0;
}
p[0] += shift;
}
updateBBox(bbox, p);
}
function resetBBox(bbox) {
bbox[0] = bbox[1] = Infinity;
bbox[2] = bbox[3] = -Infinity;
}
function getTilePoints(geometry, pointBBox, polyBBox, canonical) {
const worldSize = Math.pow(2, canonical.z) * EXTENT;
const shifts = [canonical.x * EXTENT, canonical.y * EXTENT];
const tilePoints = [];
for (const points of geometry) {
for (const point of points) {
const p = [point.x + shifts[0], point.y + shifts[1]];
updatePoint(p, pointBBox, polyBBox, worldSize);
tilePoints.push(p);
}
}
return tilePoints;
}
function getTileLines(geometry, lineBBox, polyBBox, canonical) {
const worldSize = Math.pow(2, canonical.z) * EXTENT;
const shifts = [canonical.x * EXTENT, canonical.y * EXTENT];
const tileLines = [];
for (const line of geometry) {
const tileLine = [];
for (const point of line) {
const p = [point.x + shifts[0], point.y + shifts[1]];
updateBBox(lineBBox, p);
tileLine.push(p);
}
tileLines.push(tileLine);
}
if (lineBBox[2] - lineBBox[0] <= worldSize / 2) {
resetBBox(lineBBox);
for (const line of tileLines) {
for (const p of line) {
updatePoint(p, lineBBox, polyBBox, worldSize);
}
}
}
return tileLines;
}
function pointsWithinPolygons(ctx, polygonGeometry) {
const pointBBox = [Infinity, Infinity, -Infinity, -Infinity];
const polyBBox = [Infinity, Infinity, -Infinity, -Infinity];
const canonical = ctx.canonicalID();
if (polygonGeometry.type === 'Polygon') {
const tilePolygon = getTilePolygon(polygonGeometry.coordinates, polyBBox, canonical);
const tilePoints = getTilePoints(ctx.geometry(), pointBBox, polyBBox, canonical);
if (!boxWithinBox(pointBBox, polyBBox))
return false;
for (const point of tilePoints) {
if (!pointWithinPolygon(point, tilePolygon))
return false;
}
}
if (polygonGeometry.type === 'MultiPolygon') {
const tilePolygons = getTilePolygons(polygonGeometry.coordinates, polyBBox, canonical);
const tilePoints = getTilePoints(ctx.geometry(), pointBBox, polyBBox, canonical);
if (!boxWithinBox(pointBBox, polyBBox))
return false;
for (const point of tilePoints) {
if (!pointWithinPolygons(point, tilePolygons))
return false;
}
}
return true;
}
function linesWithinPolygons(ctx, polygonGeometry) {
const lineBBox = [Infinity, Infinity, -Infinity, -Infinity];
const polyBBox = [Infinity, Infinity, -Infinity, -Infinity];
const canonical = ctx.canonicalID();
if (polygonGeometry.type === 'Polygon') {
const tilePolygon = getTilePolygon(polygonGeometry.coordinates, polyBBox, canonical);
const tileLines = getTileLines(ctx.geometry(), lineBBox, polyBBox, canonical);
if (!boxWithinBox(lineBBox, polyBBox))
return false;
for (const line of tileLines) {
if (!lineStringWithinPolygon(line, tilePolygon))
return false;
}
}
if (polygonGeometry.type === 'MultiPolygon') {
const tilePolygons = getTilePolygons(polygonGeometry.coordinates, polyBBox, canonical);
const tileLines = getTileLines(ctx.geometry(), lineBBox, polyBBox, canonical);
if (!boxWithinBox(lineBBox, polyBBox))
return false;
for (const line of tileLines) {
if (!lineStringWithinPolygons(line, tilePolygons))
return false;
}
}
return true;
}
class Within {
constructor(geojson, geometries) {
this.type = BooleanType;
this.geojson = geojson;
this.geometries = geometries;
}
static parse(args, context) {
if (args.length !== 2)
return context.error(`'within' expression requires exactly one argument, but found ${args.length - 1} instead.`);
if (isValue(args[1])) {
const geojson = args[1];
if (geojson.type === 'FeatureCollection') {
const polygonsCoords = [];
for (const polygon of geojson.features) {
const { type, coordinates } = polygon.geometry;
if (type === 'Polygon') {
polygonsCoords.push(coordinates);
}
if (type === 'MultiPolygon') {
polygonsCoords.push(...coordinates);
}
}
if (polygonsCoords.length) {
const multipolygonWrapper = {
type: 'MultiPolygon',
coordinates: polygonsCoords
};
return new Within(geojson, multipolygonWrapper);
}
}
else if (geojson.type === 'Feature') {
const type = geojson.geometry.type;
if (type === 'Polygon' || type === 'MultiPolygon') {
return new Within(geojson, geojson.geometry);
}
}
else if (geojson.type === 'Polygon' || geojson.type === 'MultiPolygon') {
return new Within(geojson, geojson);
}
}
return context.error('\'within\' expression requires valid geojson object that contains polygon geometry type.');
}
evaluate(ctx) {
if (ctx.geometry() != null && ctx.canonicalID() != null) {
if (ctx.geometryType() === 'Point') {
return pointsWithinPolygons(ctx, this.geometries);
}
else if (ctx.geometryType() === 'LineString') {
return linesWithinPolygons(ctx, this.geometries);
}
}
return false;
}
eachChild() { }
outputDefined() {
return true;
}
}
class TinyQueue {
constructor(data = [], compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0)) {
this.data = data;
this.length = this.data.length;
this.compare = compare;
if (this.length > 0) {
for (let i = (this.length >> 1) - 1; i >= 0; i--) this._down(i);
}
}
push(item) {
this.data.push(item);
this._up(this.length++);
}
pop() {
if (this.length === 0) return undefined;
const top = this.data[0];
const bottom = this.data.pop();
if (--this.length > 0) {
this.data[0] = bottom;
this._down(0);
}
return top;
}
peek() {
return this.data[0];
}
_up(pos) {
const {data, compare} = this;
const item = data[pos];
while (pos > 0) {
const parent = (pos - 1) >> 1;
const current = data[parent];
if (compare(item, current) >= 0) break;
data[pos] = current;
pos = parent;
}
data[pos] = item;
}
_down(pos) {
const {data, compare} = this;
const halfLength = this.length >> 1;
const item = data[pos];
while (pos < halfLength) {
let bestChild = (pos << 1) + 1; // initially it is the left child
const right = bestChild + 1;
if (right < this.length && compare(data[right], data[bestChild]) < 0) {
bestChild = right;
}
if (compare(data[bestChild], item) >= 0) break;
data[pos] = data[bestChild];
pos = bestChild;
}
data[pos] = item;
}
}
/**
* Rearranges items so that all items in the [left, k] are the smallest.
* The k-th element will have the (k - left + 1)-th smallest value in [left, right].
*
* @template T
* @param {T[]} arr the array to partially sort (in place)
* @param {number} k middle index for partial sorting (as defined above)
* @param {number} [left=0] left index of the range to sort
* @param {number} [right=arr.length-1] right index
* @param {(a: T, b: T) => number} [compare = (a, b) => a - b] compare function
*/
function quickselect(arr, k, left = 0, right = arr.length - 1, compare = defaultCompare) {
while (right > left) {
if (right - left > 600) {
const n = right - left + 1;
const m = k - left + 1;
const z = Math.log(n);
const s = 0.5 * Math.exp(2 * z / 3);
const sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
const newLeft = Math.max(left, Math.floor(k - m * s / n + sd));
const newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));
quickselect(arr, k, newLeft, newRight, compare);
}
const t = arr[k];
let i = left;
/** @type {number} */
let j = right;
swap(arr, left, k);
if (compare(arr[right], t) > 0) swap(arr, left, right);
while (i < j) {
swap(arr, i, j);
i++;
j--;
while (compare(arr[i], t) < 0) i++;
while (compare(arr[j], t) > 0) j--;
}
if (compare(arr[left], t) === 0) swap(arr, left, j);
else {
j++;
swap(arr, j, right);
}
if (j <= k) left = j + 1;
if (k <= j) right = j - 1;
}
}
/**
* @template T
* @param {T[]} arr
* @param {number} i
* @param {number} j
*/
function swap(arr, i, j) {
const tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
/**
* @template T
* @param {T} a
* @param {T} b
* @returns {number}
*/
function defaultCompare(a, b) {
return a < b ? -1 : a > b ? 1 : 0;
}
/**
* Classifies an array of rings into polygons with outer rings and holes
* @param rings - the rings to classify
* @param maxRings - the maximum number of rings to include in a polygon, use 0 to include all rings
* @returns an array of polygons with internal rings as holes
*/
function classifyRings(rings, maxRings) {
const len = rings.length;
if (len <= 1)
return [rings];
const polygons = [];
let polygon;
let ccw;
for (const ring of rings) {
const area = calculateSignedArea(ring);
if (area === 0)
continue;
ring.area = Math.abs(area);
if (ccw === undefined)
ccw = area < 0;
if (ccw === area < 0) {
if (polygon)
polygons.push(polygon);
polygon = [ring];
}
else {
polygon.push(ring);
}
}
if (polygon)
polygons.push(polygon);
// Earcut performance degrades with the # of rings in a polygon. For this
// reason, we limit strip out all but the `maxRings` largest rings.
if (maxRings > 1) {
for (let j = 0; j < polygons.length; j++) {
if (polygons[j].length <= maxRings)
continue;
quickselect(polygons[j], maxRings, 1, polygons[j].length - 1, compareAreas);
polygons[j] = polygons[j].slice(0, maxRings);
}
}
return polygons;
}
function compareAreas(a, b) {
return b.area - a.area;
}
/**
* Returns the signed area for the polygon ring. Positive areas are exterior rings and
* have a clockwise winding. Negative areas are interior rings and have a counter clockwise
* ordering.
*
* @param ring - Exterior or interior ring
* @returns Signed area
*/
function calculateSignedArea(ring) {
let sum = 0;
for (let i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) {
p1 = ring[i];
p2 = ring[j];
sum += (p2.x - p1.x) * (p1.y + p2.y);
}
return sum;
}
// This is taken from https://github.com/mapbox/cheap-ruler/ in order to take only the relevant parts
// Values that define WGS84 ellipsoid model of the Earth
const RE = 6378.137; // equatorial radius
const FE = 1 / 298.257223563; // flattening
const E2 = FE * (2 - FE);
const RAD = Math.PI / 180;
class CheapRuler {
constructor(lat) {
// Curvature formulas from https://en.wikipedia.org/wiki/Earth_radius#Meridional
const m = RAD * RE * 1000;
const coslat = Math.cos(lat * RAD);
const w2 = 1 / (1 - E2 * (1 - coslat * coslat));
const w = Math.sqrt(w2);
// multipliers for converting longitude and latitude degrees into distance
this.kx = m * w * coslat; // based on normal radius of curvature
this.ky = m * w * w2 * (1 - E2); // based on meridional radius of curvature
}
/**
* Given two points of the form [longitude, latitude], returns the distance.
*
* @param a - point [longitude, latitude]
* @param b - point [longitude, latitude]
* @returns distance
* @example
* const distance = ruler.distance([30.5, 50.5], [30.51, 50.49]);
* //=distance
*/
distance(a, b) {
const dx = this.wrap(a[0] - b[0]) * this.kx;
const dy = (a[1] - b[1]) * this.ky;
return Math.sqrt(dx * dx + dy * dy);
}
/**
* Returns an object of the form {point, index, t}, where point is closest point on the line
* from the given point, index is the start index of the segment with the closest point,
* and t is a parameter from 0 to 1 that indicates where the closest point is on that segment.
*
* @param line - an array of points that form the line
* @param p - point [longitude, latitude]
* @returns the nearest point, its index in the array and the proportion along the line
* @example
* const point = ruler.pointOnLine(line, [-67.04, 50.5]).point;
* //=point
*/
pointOnLine(line, p) {
let minDist = Infinity;
let minX, minY, minI, minT;
for (let i = 0; i < line.length - 1; i++) {
let x = line[i][0];
let y = line[i][1];
let dx = this.wrap(line[i + 1][0] - x) * this.kx;
let dy = (line[i + 1][1] - y) * this.ky;
let t = 0;
if (dx !== 0 || dy !== 0) {
t = (this.wrap(p[0] - x) * this.kx * dx + (p[1] - y) * this.ky * dy) / (dx * dx + dy * dy);
if (t > 1) {
x = line[i + 1][0];
y = line[i + 1][1];
}
else if (t > 0) {
x += (dx / this.kx) * t;
y += (dy / this.ky) * t;
}
}
dx = this.wrap(p[0] - x) * this.kx;
dy = (p[1] - y) * this.ky;
const sqDist = dx * dx + dy * dy;
if (sqDist < minDist) {
minDist = sqDist;
minX = x;
minY = y;
minI = i;
minT = t;
}
}
return {
point: [minX, minY],
index: minI,
t: Math.max(0, Math.min(1, minT))
};
}
wrap(deg) {
while (deg < -180)
deg += 360;
while (deg > 180)
deg -= 360;
return deg;
}
}
const MinPointsSize = 100;
const MinLinePointsSize = 50;
function compareDistPair(a, b) {
return b[0] - a[0];
}
function getRangeSize(range) {
return range[1] - range[0] + 1;
}
function isRangeSafe(range, threshold) {
return range[1] >= range[0] && range[1] < threshold;
}
function splitRange(range, isLine) {
if (range[0] > range[1]) {
return [null, null];
}
const size = getRangeSize(range);
if (isLine) {
if (size === 2) {
return [range, null];
}
const size1 = Math.floor(size / 2);
return [[range[0], range[0] + size1],
[range[0] + size1, range[1]]];
}
if (size === 1) {
return [range, null];
}
const size1 = Math.floor(size / 2) - 1;
return [[range[0], range[0] + size1],
[range[0] + size1 + 1, range[1]]];
}
function getBBox(coords, range) {
if (!isRangeSafe(range, coords.length)) {
return [Infinity, Infinity, -Infinity, -Infinity];
}
const bbox = [Infinity, Infinity, -Infinity, -Infinity];
for (let i = range[0]; i <= range[1]; ++i) {
updateBBox(bbox, coords[i]);
}
return bbox;
}
function getPolygonBBox(polygon) {
const bbox = [Infinity, Infinity, -Infinity, -Infinity];
for (const ring of polygon) {
for (const coord of ring) {
updateBBox(bbox, coord);
}
}
return bbox;
}
function isValidBBox(bbox) {
return bbox[0] !== -Infinity && bbox[1] !== -Infinity && bbox[2] !== Infinity && bbox[3] !== Infinity;
}
// Calculate the distance between two bounding boxes.
// Calculate the delta in x and y direction, and use two fake points {0.0, 0.0}
// and {dx, dy} to calculate the distance. Distance will be 0.0 if bounding box are overlapping.
function bboxToBBoxDistance(bbox1, bbox2, ruler) {
if (!isValidBBox(bbox1) || !isValidBBox(bbox2)) {
return NaN;
}
let dx = 0.0;
let dy = 0.0;
// bbox1 in left side
if (bbox1[2] < bbox2[0]) {
dx = bbox2[0] - bbox1[2];
}
// bbox1 in right side
if (bbox1[0] > bbox2[2]) {
dx = bbox1[0] - bbox2[2];
}
// bbox1 in above side
if (bbox1[1] > bbox2[3]) {
dy = bbox1[1] - bbox2[3];
}
// bbox1 in down side
if (bbox1[3] < bbox2[1]) {
dy = bbox2[1] - bbox1[3];
}
return ruler.distance([0.0, 0.0], [dx, dy]);
}
function pointToLineDistance(point, line, ruler) {
const nearestPoint = ruler.pointOnLine(line, point);
return ruler.distance(point, nearestPoint.point);
}
function segmentToSegmentDistance(p1, p2, q1, q2, ruler) {
const dist1 = Math.min(pointToLineDistance(p1, [q1, q2], ruler), pointToLineDistance(p2, [q1, q2], ruler));
const dist2 = Math.min(pointToLineDistance(q1, [p1, p2], ruler), pointToLineDistance(q2, [p1, p2], ruler));
return Math.min(dist1, dist2);
}
function lineToLineDistance(line1, range1, line2, range2, ruler) {
const rangeSafe = isRangeSafe(range1, line1.length) && isRangeSafe(range2, line2.length);
if (!rangeSafe) {
return Infinity;
}
let dist = Infinity;
for (let i = range1[0]; i < range1[1]; ++i) {
const p1 = line1[i];
const p2 = line1[i + 1];
for (let j = range2[0]; j < range2[1]; ++j) {
const q1 = line2[j];
const q2 = line2[j + 1];
if (segmentIntersectSegment(p1, p2, q1, q2)) {
return 0.0;
}
dist = Math.min(dist, segmentToSegmentDistance(p1, p2, q1, q2, ruler));
}
}
return dist;
}
function pointsToPointsDistance(points1, range1, points2, range2, ruler) {
const rangeSafe = isRangeSafe(range1, points1.length) && isRangeSafe(range2, points2.length);
if (!rangeSafe) {
return NaN;
}
let dist = Infinity;
for (let i = range1[0]; i <= range1[1]; ++i) {
for (let j = range2[0]; j <= range2[1]; ++j) {
dist = Math.min(dist, ruler.distance(points1[i], points2[j]));
if (dist === 0.0) {
return dist;
}
}
}
return dist;
}
function pointToPolygonDistance(point, polygon, ruler) {
if (pointWithinPolygon(point, polygon, true)) {
return 0.0;
}
let dist = Infinity;
for (const ring of polygon) {
const front = ring[0];
const back = ring[ring.length - 1];
if (front !== back) {
dist = Math.min(dist, pointToLineDistance(point, [back, front], ruler));
if (dist === 0.0) {
return dist;
}
}
const nearestPoint = ruler.pointOnLine(ring, point);
dist = Math.min(dist, ruler.distance(point, nearestPoint.point));
if (dist === 0.0) {
return dist;
}
}
return dist;
}
function lineToPolygonDistance(line, range, polygon, ruler) {
if (!isRangeSafe(range, line.length)) {
return NaN;
}
for (let i = range[0]; i <= range[1]; ++i) {
if (pointWithinPolygon(line[i], polygon, true)) {
return 0.0;
}
}
let dist = Infinity;
for (let i = range[0]; i < range[1]; ++i) {
const p1 = line[i];
const p2 = line[i + 1];
for (const ring of polygon) {
for (let j = 0, len = ring.length, k = len - 1; j < len; k = j++) {
const q1 = ring[k];
const q2 = ring[j];
if (segmentIntersectSegment(p1, p2, q1, q2)) {
return 0.0;
}
dist = Math.min(dist, segmentToSegmentDistance(p1, p2, q1, q2, ruler));
}
}
}
return dist;
}
function polygonIntersect(poly1, poly2) {
for (const ring of poly1) {
for (const point of ring) {
if (pointWithinPolygon(point, poly2, true)) {
return true;
}
}
}
return false;
}
function polygonToPolygonDistance(polygon1, polygon2, ruler, currentMiniDist = Infinity) {
const bbox1 = getPolygonBBox(polygon1);
const bbox2 = getPolygonBBox(polygon2);
if (currentMiniDist !== Infinity && bboxToBBoxDistance(bbox1, bbox2, ruler) >= currentMiniDist) {
return currentMiniDist;
}
if (boxWithinBox(bbox1, bbox2)) {
if (polygonIntersect(polygon1, polygon2)) {
return 0.0;
}
}
else if (polygonIntersect(polygon2, polygon1)) {
return 0.0;
}
let dist = Infinity;
for (const ring1 of polygon1) {
for (let i = 0, len1 = ring1.length, l = len1 - 1; i < len1; l = i++) {
const p1 = ring1[l];
const p2 = ring1[i];
for (const ring2 of polygon2) {
for (let j = 0, len2 = ring2.length, k = len2 - 1; j < len2; k = j++) {
const q1 = ring2[k];
const q2 = ring2[j];
if (segmentIntersectSegment(p1, p2, q1, q2)) {
return 0.0;
}
dist = Math.min(dist, segmentToSegmentDistance(p1, p2, q1, q2, ruler));
}
}
}
}
return dist;
}
function updateQueue(distQueue, miniDist, ruler, points, polyBBox, rangeA) {
if (!rangeA) {
return;
}
const tempDist = bboxToBBoxDistance(getBBox(points, rangeA), polyBBox, ruler);
// Insert new pair to the queue if the bbox distance is less than
// miniDist, The pair with biggest distance will be at the top
if (tempDist < miniDist) {
distQueue.push([tempDist, rangeA, [0, 0]]);
}
}
function updateQueueTwoSets(distQueue, miniDist, ruler, pointSet1, pointSet2, range1, range2) {
if (!range1 || !range2) {
return;
}
const tempDist = bboxToBBoxDistance(getBBox(pointSet1, range1), getBBox(pointSet2, range2), ruler);
// Insert new pair to the queue if the bbox distance is less than
// miniDist, The pair with biggest distance will be at the top
if (tempDist < miniDist) {
distQueue.push([tempDist, range1, range2]);
}
}
// Divide and conquer, the time complexity is O(n*lgn), faster than Brute force
// O(n*n) Most of the time, use index for in-place processing.
function pointsToPolygonDistance(points, isLine, polygon, ruler, currentMiniDist = Infinity) {
let miniDist = Math.min(ruler.distance(points[0], polygon[0][0]), currentMiniDist);
if (miniDist === 0.0) {
return miniDist;
}
const distQueue = new TinyQueue([[0, [0, points.length - 1], [0, 0]]], compareDistPair);
const polyBBox = getPolygonBBox(polygon);
while (distQueue.length > 0) {
const distPair = distQueue.pop();
if (distPair[0] >= miniDist) {
continue;
}
const range = distPair[1];
// In case the set size are relatively small, we could use brute-force directly
const threshold = isLine ? MinLinePointsSize : MinPointsSize;
if (getRangeSize(range) <= threshold) {
if (!isRangeSafe(range, points.length)) {
return NaN;
}
if (isLine) {
const tempDist = lineToPolygonDistance(points, range, polygon, ruler);
if (isNaN(tempDist) || tempDist === 0.0) {
return tempDist;
}
miniDist = Math.min(miniDist, tempDist);
}
else {
for (let i = range[0]; i <= range[1]; ++i) {
const tempDist = pointToPolygonDistance(points[i], polygon, ruler);
miniDist = Math.min(miniDist, tempDist);
if (miniDist === 0.0) {
return 0.0;
}
}
}
}
else {
const newRangesA = splitRange(range, isLine);
updateQueue(distQueue, miniDist, ruler, points, polyBBox, newRangesA[0]);
updateQueue(distQueue, miniDist, ruler, points, polyBBox, newRangesA[1]);
}
}
return miniDist;
}
function pointSetToPointSetDistance(pointSet1, isLine1, pointSet2, isLine2, ruler, currentMiniDist = Infinity) {
let miniDist = Math.min(currentMiniDist, ruler.distance(pointSet1[0], pointSet2[0]));
if (miniDist === 0.0) {
return miniDist;
}
const distQueue = new TinyQueue([[0, [0, pointSet1.length - 1], [0, pointSet2.length - 1]]], compareDistPair);
while (distQueue.length > 0) {
const distPair = distQueue.pop();
if (distPair[0] >= miniDist) {
continue;
}
const rangeA = distPair[1];
const rangeB = distPair[2];
const threshold1 = isLine1 ? MinLinePointsSize : MinPointsSize;
const threshold2 = isLine2 ? MinLinePointsSize : MinPointsSize;
// In case the set size are relatively small, we could use brute-force directly
if (getRangeSize(rangeA) <= threshold1 && getRangeSize(rangeB) <= threshold2) {
if (!isRangeSafe(rangeA, pointSet1.length) && isRangeSafe(rangeB, pointSet2.length)) {
return NaN;
}
let tempDist;
if (isLine1 && isLine2) {
tempDist = lineToLineDistance(pointSet1, rangeA, pointSet2, rangeB, ruler);
miniDist = Math.min(miniDist, tempDist);
}
else if (isLine1 && !isLine2) {
const sublibe = pointSet1.slice(rangeA[0], rangeA[1] + 1);
for (let i = rangeB[0]; i <= rangeB[1]; ++i) {
tempDist = pointToLineDistance(pointSet2[i], sublibe, ruler);
miniDist = Math.min(miniDist, tempDist);
if (miniDist === 0.0) {
return miniDist;
}
}
}
else if (!isLine1 && isLine2) {
const sublibe = pointSet2.slice(rangeB[0], rangeB[1] + 1);
for (let i = rangeA[0]; i <= rangeA[1]; ++i) {
tempDist = pointToLineDistance(pointSet1[i], sublibe, ruler);
miniDist = Math.min(miniDist, tempDist);
if (miniDist === 0.0) {
return miniDist;
}
}
}
else {
tempDist = pointsToPointsDistance(pointSet1, rangeA, pointSet2, rangeB, ruler);
miniDist = Math.min(miniDist, tempDist);
}
}
else {
const newRangesA = splitRange(rangeA, isLine1);
const newRangesB = splitRange(rangeB, isLine2);
updateQueueTwoSets(distQueue, miniDist, ruler, pointSet1, pointSet2, newRangesA[0], newRangesB[0]);
updateQueueTwoSets(distQueue, miniDist, ruler, pointSet1, pointSet2, newRangesA[0], newRangesB[1]);
updateQueueTwoSets(distQueue, miniDist, ruler, pointSet1, pointSet2, newRangesA[1], newRangesB[0]);
updateQueueTwoSets(distQueue, miniDist, ruler, pointSet1, pointSet2, newRangesA[1], newRangesB[1]);
}
}
return miniDist;
}
function pointToGeometryDistance(ctx, geometries) {
const tilePoints = ctx.geometry();
const pointPosition = tilePoints.flat().map(p => getLngLatFromTileCoord([p.x, p.y], ctx.canonical));
if (tilePoints.length === 0) {
return NaN;
}
const ruler = new CheapRuler(pointPosition[0][1]);
let dist = Infinity;
for (const geometry of geometries) {
switch (geometry.type) {
case 'Point':
dist = Math.min(dist, pointSetToPointSetDistance(pointPosition, false, [geometry.coordinates], false, ruler, dist));
break;
case 'LineString':
dist = Math.min(dist, pointSetToPointSetDistance(pointPosition, false, geometry.coordinates, true, ruler, dist));
break;
case 'Polygon':
dist = Math.min(dist, pointsToPolygonDistance(pointPosition, false, geometry.coordinates, ruler, dist));
break;
}
if (dist === 0.0) {
return dist;
}
}
return dist;
}
function lineStringToGeometryDistance(ctx, geometries) {
const tileLine = ctx.geometry();
const linePositions = tileLine.flat().map(p => getLngLatFromTileCoord([p.x, p.y], ctx.canonical));
if (tileLine.length === 0) {
return NaN;
}
const ruler = new CheapRuler(linePositions[0][1]);
let dist = Infinity;
for (const geometry of geometries) {
switch (geometry.type) {
case 'Point':
dist = Math.min(dist, pointSetToPointSetDistance(linePositions, true, [geometry.coordinates], false, ruler, dist));
break;
case 'LineString':
dist = Math.min(dist, pointSetToPointSetDistance(linePositions, true, geometry.coordinates, true, ruler, dist));
break;
case 'Polygon':
dist = Math.min(dist, pointsToPolygonDistance(linePositions, true, geometry.coordinates, ruler, dist));
break;
}
if (dist === 0.0) {
return dist;
}
}
return dist;
}
function polygonToGeometryDistance(ctx, geometries) {
const tilePolygon = ctx.geometry();
if (tilePolygon.length === 0 || tilePolygon[0].length === 0) {
return NaN;
}
const polygons = classifyRings(tilePolygon, 0).map(polygon => {
return polygon.map(ring => {
return ring.map(p => getLngLatFromTileCoord([p.x, p.y], ctx.canonical));
});
});
const ruler = new CheapRuler(polygons[0][0][0][1]);
let dist = Infinity;
for (const geometry of geometries) {
for (const polygon of polygons) {
switch (geometry.type) {
case 'Point':
dist = Math.min(dist, pointsToPolygonDistance([geometry.coordinates], false, polygon, ruler, dist));
break;
case 'LineString':
dist = Math.min(dist, pointsToPolygonDistance(geometry.coordinates, true, polygon, ruler, dist));
break;
case 'Polygon':
dist = Math.min(dist, polygonToPolygonDistance(polygon, geometry.coordinates, ruler, dist));
break;
}
if (dist === 0.0) {
return dist;
}
}
}
return dist;
}
function toSimpleGeometry(geometry) {
if (geometry.type === 'MultiPolygon') {
return geometry.coordinates.map(polygon => {
return {
type: 'Polygon',
coordinates: polygon
};
});
}
if (geometry.type === 'MultiLineString') {
return geometry.coordinates.map(lineString => {
return {
type: 'LineString',
coordinates: lineString
};
});
}
if (geometry.type === 'MultiPoint') {
return geometry.coordinates.map(point => {
return {
type: 'Point',
coordinates: point
};
});
}
return [geometry];
}
class Distance {
constructor(geojson, geometries) {
this.type = NumberType;
this.geojson = geojson;
this.geometries = geometries;
}
static parse(args, context) {
if (args.length !== 2)
return context.error(`'distance' expression requires exactly one argument, but found ${args.length - 1} instead.`);
if (isValue(args[1])) {
const geojson = args[1];
if (geojson.type === 'FeatureCollection') {
return new Distance(geojson, geojson.features.map(feature => toSimpleGeometry(feature.geometry)).flat());
}
else if (geojson.type === 'Feature') {
return new Distance(geojson, toSimpleGeometry(geojson.geometry));
}
else if ('type' in geojson && 'coordinates' in geojson) {
return new Distance(geojson, toSimpleGeometry(geojson));
}
}
return context.error('\'distance\' expression requires valid geojson object that contains polygon geometry type.');
}
evaluate(ctx) {
if (ctx.geometry() != null && ctx.canonicalID() != null) {
if (ctx.geometryType() === 'Point') {
return pointToGeometryDistance(ctx, this.geometries);
}
else if (ctx.geometryType() === 'LineString') {
return lineStringToGeometryDistance(ctx, this.geometries);
}
else if (ctx.geometryType() === 'Polygon') {
return polygonToGeometryDistance(ctx, this.geometries);
}
}
return NaN;
}
eachChild() { }
outputDefined() {
return true;
}
}
class GlobalState {
constructor(key) {
this.type = ValueType;
this.key = key;
}
static parse(args, context) {
if (args.length !== 2) {
return context.error(`Expected 1 argument, but found ${args.length - 1} instead.`);
}
const key = args[1];
if (key === undefined || key === null) {
return context.error('Global state property must be defined.');
}
if (typeof key !== 'string') {
return context.error(`Global state property must be string, but found ${typeof args[1]} instead.`);
}
return new GlobalState(key);
}
evaluate(ctx) {
var _a;
const globalState = (_a = ctx.globals) === null || _a === void 0 ? void 0 : _a.globalState;
if (!globalState || Object.keys(globalState).length === 0)
return null;
return getOwn(globalState, this.key);
}
eachChild() { }
outputDefined() {
return false;
}
}
const expressions$1 = {
// special forms
'==': Equals,
'!=': NotEquals,
'>': GreaterThan,
'<': LessThan,
'>=': GreaterThanOrEqual,
'<=': LessThanOrEqual,
'array': Assertion,
'at': At,
'boolean': Assertion,
'case': Case,
'coalesce': Coalesce,
'collator': CollatorExpression,
'format': FormatExpression,
'image': ImageExpression,
'in': In,
'index-of': IndexOf,
'interpolate': Interpolate,
'interpolate-hcl': Interpolate,
'interpolate-lab': Interpolate,
'length': Length,
'let': Let,
'literal': Literal,
'match': Match,
'number': Assertion,
'number-format': NumberFormat,
'object': Assertion,
'slice': Slice,
'step': Step,
'string': Assertion,
'to-boolean': Coercion,
'to-color': Coercion,
'to-number': Coercion,
'to-string': Coercion,
'var': Var,
'within': Within,
'distance': Distance,
'global-state': GlobalState
};
class CompoundExpression {
constructor(name, type, evaluate, args) {
this.name = name;
this.type = type;
this._evaluate = evaluate;
this.args = args;
}
evaluate(ctx) {
return this._evaluate(ctx, this.args);
}
eachChild(fn) {
this.args.forEach(fn);
}
outputDefined() {
return false;
}
static parse(args, context) {
const op = args[0];
const definition = CompoundExpression.definitions[op];
if (!definition) {
return context.error(`Unknown expression "${op}". If you wanted a literal array, use ["literal", [...]].`, 0);
}
// Now check argument types against each signature
const type = Array.isArray(definition) ?
definition[0] : definition.type;
const availableOverloads = Array.isArray(definition) ?
[[definition[1], definition[2]]] :
definition.overloads;
const overloads = availableOverloads.filter(([signature]) => (!Array.isArray(signature) || // varags
signature.length === args.length - 1 // correct param count
));
let signatureContext = null;
for (const [params, evaluate] of overloads) {
// Use a fresh context for each attempted signature so that, if
// we eventually succeed, we haven't polluted `context.errors`.
signatureContext = new ParsingContext(context.registry, isExpressionConstant, context.path, null, context.scope);
// First parse all the args, potentially coercing to the
// types expected by this overload.
const parsedArgs = [];
let argParseFailed = false;
for (let i = 1; i < args.length; i++) {
const arg = args[i];
const expectedType = Array.isArray(params) ?
params[i - 1] :
params.type;
const parsed = signatureContext.parse(arg, 1 + parsedArgs.length, expectedType);
if (!parsed) {
argParseFailed = true;
break;
}
parsedArgs.push(parsed);
}
if (argParseFailed) {
// Couldn't coerce args of this overload to expected type, move
// on to next one.
continue;
}
if (Array.isArray(params)) {
if (params.length !== parsedArgs.length) {
signatureContext.error(`Expected ${params.length} arguments, but found ${parsedArgs.length} instead.`);
continue;
}
}
for (let i = 0; i < parsedArgs.length; i++) {
const expected = Array.isArray(params) ? params[i] : params.type;
const arg = parsedArgs[i];
signatureContext.concat(i + 1).checkSubtype(expected, arg.type);
}
if (signatureContext.errors.length === 0) {
return new CompoundExpression(op, type, evaluate, parsedArgs);
}
}
if (overloads.length === 1) {
context.errors.push(...signatureContext.errors);
}
else {
const expected = overloads.length ? overloads : availableOverloads;
const signatures = expected
.map(([params]) => stringifySignature(params))
.join(' | ');
const actualTypes = [];
// For error message, re-parse arguments without trying to
// apply any coercions
for (let i = 1; i < args.length; i++) {
const parsed = context.parse(args[i], 1 + actualTypes.length);
if (!parsed)
return null;
actualTypes.push(typeToString(parsed.type));
}
context.error(`Expected arguments of type ${signatures}, but found (${actualTypes.join(', ')}) instead.`);
}
return null;
}
static register(registry, definitions) {
CompoundExpression.definitions = definitions;
for (const name in definitions) {
registry[name] = CompoundExpression;
}
}
}
function rgba(ctx, [r, g, b, a]) {
r = r.evaluate(ctx);
g = g.evaluate(ctx);
b = b.evaluate(ctx);
const alpha = a ? a.evaluate(ctx) : 1;
const error = validateRGBA(r, g, b, alpha);
if (error)
throw new RuntimeError(error);
return new Color(r / 255, g / 255, b / 255, alpha, false);
}
function has(key, obj) {
return key in obj;
}
function get(key, obj) {
const v = obj[key];
return typeof v === 'undefined' ? null : v;
}
function binarySearch(v, a, i, j) {
while (i <= j) {
const m = (i + j) >> 1;
if (a[m] === v)
return true;
if (a[m] > v)
j = m - 1;
else
i = m + 1;
}
return false;
}
function varargs(type) {
return { type };
}
CompoundExpression.register(expressions$1, {
'error': [
ErrorType,
[StringType],
(ctx, [v]) => { throw new RuntimeError(v.evaluate(ctx)); }
],
'typeof': [
StringType,
[ValueType],
(ctx, [v]) => typeToString(typeOf(v.evaluate(ctx)))
],
'to-rgba': [
array(NumberType, 4),
[ColorType],
(ctx, [v]) => {
const [r, g, b, a] = v.evaluate(ctx).rgb;
return [r * 255, g * 255, b * 255, a];
},
],
'rgb': [
ColorType,
[NumberType, NumberType, NumberType],
rgba
],
'rgba': [
ColorType,
[NumberType, NumberType, NumberType, NumberType],
rgba
],
'has': {
type: BooleanType,
overloads: [
[
[StringType],
(ctx, [key]) => has(key.evaluate(ctx), ctx.properties())
], [
[StringType, ObjectType],
(ctx, [key, obj]) => has(key.evaluate(ctx), obj.evaluate(ctx))
]
]
},
'get': {
type: ValueType,
overloads: [
[
[StringType],
(ctx, [key]) => get(key.evaluate(ctx), ctx.properties())
], [
[StringType, ObjectType],
(ctx, [key, obj]) => get(key.evaluate(ctx), obj.evaluate(ctx))
]
]
},
'feature-state': [
ValueType,
[StringType],
(ctx, [key]) => get(key.evaluate(ctx), ctx.featureState || {})
],
'properties': [
ObjectType,
[],
(ctx) => ctx.properties()
],
'geometry-type': [
StringType,
[],
(ctx) => ctx.geometryType()
],
'id': [
ValueType,
[],
(ctx) => ctx.id()
],
'zoom': [
NumberType,
[],
(ctx) => ctx.globals.zoom
],
'heatmap-density': [
NumberType,
[],
(ctx) => ctx.globals.heatmapDensity || 0
],
'elevation': [
NumberType,
[],
(ctx) => ctx.globals.elevation || 0
],
'line-progress': [
NumberType,
[],
(ctx) => ctx.globals.lineProgress || 0
],
'accumulated': [
ValueType,
[],
(ctx) => ctx.globals.accumulated === undefined ? null : ctx.globals.accumulated
],
'+': [
NumberType,
varargs(NumberType),
(ctx, args) => {
let result = 0;
for (const arg of args) {
result += arg.evaluate(ctx);
}
return result;
}
],
'*': [
NumberType,
varargs(NumberType),
(ctx, args) => {
let result = 1;
for (const arg of args) {
result *= arg.evaluate(ctx);
}
return result;
}
],
'-': {
type: NumberType,
overloads: [
[
[NumberType, NumberType],
(ctx, [a, b]) => a.evaluate(ctx) - b.evaluate(ctx)
], [
[NumberType],
(ctx, [a]) => -a.evaluate(ctx)
]
]
},
'/': [
NumberType,
[NumberType, NumberType],
(ctx, [a, b]) => a.evaluate(ctx) / b.evaluate(ctx)
],
'%': [
NumberType,
[NumberType, NumberType],
(ctx, [a, b]) => a.evaluate(ctx) % b.evaluate(ctx)
],
'ln2': [
NumberType,
[],
() => Math.LN2
],
'pi': [
NumberType,
[],
() => Math.PI
],
'e': [
NumberType,
[],
() => Math.E
],
'^': [
NumberType,
[NumberType, NumberType],
(ctx, [b, e]) => Math.pow(b.evaluate(ctx), e.evaluate(ctx))
],
'sqrt': [
NumberType,
[NumberType],
(ctx, [x]) => Math.sqrt(x.evaluate(ctx))
],
'log10': [
NumberType,
[NumberType],
(ctx, [n]) => Math.log(n.evaluate(ctx)) / Math.LN10
],
'ln': [
NumberType,
[NumberType],
(ctx, [n]) => Math.log(n.evaluate(ctx))
],
'log2': [
NumberType,
[NumberType],
(ctx, [n]) => Math.log(n.evaluate(ctx)) / Math.LN2
],
'sin': [
NumberType,
[NumberType],
(ctx, [n]) => Math.sin(n.evaluate(ctx))
],
'cos': [
NumberType,
[NumberType],
(ctx, [n]) => Math.cos(n.evaluate(ctx))
],
'tan': [
NumberType,
[NumberType],
(ctx, [n]) => Math.tan(n.evaluate(ctx))
],
'asin': [
NumberType,
[NumberType],
(ctx, [n]) => Math.asin(n.evaluate(ctx))
],
'acos': [
NumberType,
[NumberType],
(ctx, [n]) => Math.acos(n.evaluate(ctx))
],
'atan': [
NumberType,
[NumberType],
(ctx, [n]) => Math.atan(n.evaluate(ctx))
],
'min': [
NumberType,
varargs(NumberType),
(ctx, args) => Math.min(...args.map(arg => arg.evaluate(ctx)))
],
'max': [
NumberType,
varargs(NumberType),
(ctx, args) => Math.max(...args.map(arg => arg.evaluate(ctx)))
],
'abs': [
NumberType,
[NumberType],
(ctx, [n]) => Math.abs(n.evaluate(ctx))
],
'round': [
NumberType,
[NumberType],
(ctx, [n]) => {
const v = n.evaluate(ctx);
// Javascript's Math.round() rounds towards +Infinity for halfway
// values, even when they're negative. It's more common to round
// away from 0 (e.g., this is what python and C++ do)
return v < 0 ? -Math.round(-v) : Math.round(v);
}
],
'floor': [
NumberType,
[NumberType],
(ctx, [n]) => Math.floor(n.evaluate(ctx))
],
'ceil': [
NumberType,
[NumberType],
(ctx, [n]) => Math.ceil(n.evaluate(ctx))
],
'filter-==': [
BooleanType,
[StringType, ValueType],
(ctx, [k, v]) => ctx.properties()[k.value] === v.value
],
'filter-id-==': [
BooleanType,
[ValueType],
(ctx, [v]) => ctx.id() === v.value
],
'filter-type-==': [
BooleanType,
[StringType],
(ctx, [v]) => ctx.geometryType() === v.value
],
'filter-<': [
BooleanType,
[StringType, ValueType],
(ctx, [k, v]) => {
const a = ctx.properties()[k.value];
const b = v.value;
return typeof a === typeof b && a < b;
}
],
'filter-id-<': [
BooleanType,
[ValueType],
(ctx, [v]) => {
const a = ctx.id();
const b = v.value;
return typeof a === typeof b && a < b;
}
],
'filter->': [
BooleanType,
[StringType, ValueType],
(ctx, [k, v]) => {
const a = ctx.properties()[k.value];
const b = v.value;
return typeof a === typeof b && a > b;
}
],
'filter-id->': [
BooleanType,
[ValueType],
(ctx, [v]) => {
const a = ctx.id();
const b = v.value;
return typeof a === typeof b && a > b;
}
],
'filter-<=': [
BooleanType,
[StringType, ValueType],
(ctx, [k, v]) => {
const a = ctx.properties()[k.value];
const b = v.value;
return typeof a === typeof b && a <= b;
}
],
'filter-id-<=': [
BooleanType,
[ValueType],
(ctx, [v]) => {
const a = ctx.id();
const b = v.value;
return typeof a === typeof b && a <= b;
}
],
'filter->=': [
BooleanType,
[StringType, ValueType],
(ctx, [k, v]) => {
const a = ctx.properties()[k.value];
const b = v.value;
return typeof a === typeof b && a >= b;
}
],
'filter-id->=': [
BooleanType,
[ValueType],
(ctx, [v]) => {
const a = ctx.id();
const b = v.value;
return typeof a === typeof b && a >= b;
}
],
'filter-has': [
BooleanType,
[ValueType],
(ctx, [k]) => k.value in ctx.properties()
],
'filter-has-id': [
BooleanType,
[],
(ctx) => (ctx.id() !== null && ctx.id() !== undefined)
],
'filter-type-in': [
BooleanType,
[array(StringType)],
(ctx, [v]) => v.value.indexOf(ctx.geometryType()) >= 0
],
'filter-id-in': [
BooleanType,
[array(ValueType)],
(ctx, [v]) => v.value.indexOf(ctx.id()) >= 0
],
'filter-in-small': [
BooleanType,
[StringType, array(ValueType)],
// assumes v is an array literal
(ctx, [k, v]) => v.value.indexOf(ctx.properties()[k.value]) >= 0
],
'filter-in-large': [
BooleanType,
[StringType, array(ValueType)],
// assumes v is a array literal with values sorted in ascending order and of a single type
(ctx, [k, v]) => binarySearch(ctx.properties()[k.value], v.value, 0, v.value.length - 1)
],
'all': {
type: BooleanType,
overloads: [
[
[BooleanType, BooleanType],
(ctx, [a, b]) => a.evaluate(ctx) && b.evaluate(ctx)
],
[
varargs(BooleanType),
(ctx, args) => {
for (const arg of args) {
if (!arg.evaluate(ctx))
return false;
}
return true;
}
]
]
},
'any': {
type: BooleanType,
overloads: [
[
[BooleanType, BooleanType],
(ctx, [a, b]) => a.evaluate(ctx) || b.evaluate(ctx)
],
[
varargs(BooleanType),
(ctx, args) => {
for (const arg of args) {
if (arg.evaluate(ctx))
return true;
}
return false;
}
]
]
},
'!': [
BooleanType,
[BooleanType],
(ctx, [b]) => !b.evaluate(ctx)
],
'is-supported-script': [
BooleanType,
[StringType],
// At parse time this will always return true, so we need to exclude this expression with isGlobalPropertyConstant
(ctx, [s]) => {
const isSupportedScript = ctx.globals && ctx.globals.isSupportedScript;
if (isSupportedScript) {
return isSupportedScript(s.evaluate(ctx));
}
return true;
}
],
'upcase': [
StringType,
[StringType],
(ctx, [s]) => s.evaluate(ctx).toUpperCase()
],
'downcase': [
StringType,
[StringType],
(ctx, [s]) => s.evaluate(ctx).toLowerCase()
],
'concat': [
StringType,
varargs(ValueType),
(ctx, args) => args.map(arg => valueToString(arg.evaluate(ctx))).join('')
],
'resolved-locale': [
StringType,
[CollatorType],
(ctx, [collator]) => collator.evaluate(ctx).resolvedLocale()
]
});
function stringifySignature(signature) {
if (Array.isArray(signature)) {
return `(${signature.map(typeToString).join(', ')})`;
}
else {
return `(${typeToString(signature.type)}...)`;
}
}
function isExpressionConstant(expression) {
if (expression instanceof Var) {
return isExpressionConstant(expression.boundExpression);
}
else if (expression instanceof CompoundExpression && expression.name === 'error') {
return false;
}
else if (expression instanceof CollatorExpression) {
// Although the results of a Collator expression with fixed arguments
// generally shouldn't change between executions, we can't serialize them
// as constant expressions because results change based on environment.
return false;
}
else if (expression instanceof Within) {
return false;
}
else if (expression instanceof Distance) {
return false;
}
else if (expression instanceof GlobalState) {
return false;
}
const isTypeAnnotation = expression instanceof Coercion ||
expression instanceof Assertion;
let childrenConstant = true;
expression.eachChild(child => {
// We can _almost_ assume that if `expressions` children are constant,
// they would already have been evaluated to Literal values when they
// were parsed. Type annotations are the exception, because they might
// have been inferred and added after a child was parsed.
// So we recurse into isConstant() for the children of type annotations,
// but otherwise simply check whether they are Literals.
if (isTypeAnnotation) {
childrenConstant = childrenConstant && isExpressionConstant(child);
}
else {
childrenConstant = childrenConstant && child instanceof Literal;
}
});
if (!childrenConstant) {
return false;
}
return isFeatureConstant(expression) &&
isGlobalPropertyConstant(expression, ['zoom', 'heatmap-density', 'elevation', 'line-progress', 'accumulated', 'is-supported-script']);
}
function isFeatureConstant(e) {
if (e instanceof CompoundExpression) {
if (e.name === 'get' && e.args.length === 1) {
return false;
}
else if (e.name === 'feature-state') {
return false;
}
else if (e.name === 'has' && e.args.length === 1) {
return false;
}
else if (e.name === 'properties' ||
e.name === 'geometry-type' ||
e.name === 'id') {
return false;
}
else if (/^filter-/.test(e.name)) {
return false;
}
}
if (e instanceof Within) {
return false;
}
if (e instanceof Distance) {
return false;
}
let result = true;
e.eachChild(arg => {
if (result && !isFeatureConstant(arg)) {
result = false;
}
});
return result;
}
function isStateConstant(e) {
if (e instanceof CompoundExpression) {
if (e.name === 'feature-state') {
return false;
}
}
let result = true;
e.eachChild(arg => {
if (result && !isStateConstant(arg)) {
result = false;
}
});
return result;
}
function isGlobalPropertyConstant(e, properties) {
if (e instanceof CompoundExpression && properties.indexOf(e.name) >= 0) {
return false;
}
let result = true;
e.eachChild((arg) => {
if (result && !isGlobalPropertyConstant(arg, properties)) {
result = false;
}
});
return result;
}
function success(value) {
return { result: 'success', value };
}
function error(value) {
return { result: 'error', value };
}
function supportsPropertyExpression(spec) {
return spec['property-type'] === 'data-driven' || spec['property-type'] === 'cross-faded-data-driven';
}
function supportsZoomExpression(spec) {
return !!spec.expression && spec.expression.parameters.indexOf('zoom') > -1;
}
function supportsInterpolation(spec) {
return !!spec.expression && spec.expression.interpolated;
}
function getType(val) {
if (val instanceof Number) {
return 'number';
}
else if (val instanceof String) {
return 'string';
}
else if (val instanceof Boolean) {
return 'boolean';
}
else if (Array.isArray(val)) {
return 'array';
}
else if (val === null) {
return 'null';
}
else {
return typeof val;
}
}
function isFunction$1(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value) && typeOf(value) === ObjectType;
}
function identityFunction(x) {
return x;
}
function getParseFunction(propertySpec) {
switch (propertySpec.type) {
case 'color':
return Color.parse;
case 'padding':
return Padding.parse;
case 'numberArray':
return NumberArray.parse;
case 'colorArray':
return ColorArray.parse;
default:
return null;
}
}
function getInnerFunction(type) {
switch (type) {
case 'exponential':
return evaluateExponentialFunction;
case 'interval':
return evaluateIntervalFunction;
case 'categorical':
return evaluateCategoricalFunction;
case 'identity':
return evaluateIdentityFunction;
default:
throw new Error(`Unknown function type "${type}"`);
}
}
function createFunction(parameters, propertySpec) {
const zoomAndFeatureDependent = parameters.stops && typeof parameters.stops[0][0] === 'object';
const featureDependent = zoomAndFeatureDependent || parameters.property !== undefined;
const zoomDependent = zoomAndFeatureDependent || !featureDependent;
const type = parameters.type || (supportsInterpolation(propertySpec) ? 'exponential' : 'interval');
const parseFn = getParseFunction(propertySpec);
if (parseFn) {
parameters = extendBy({}, parameters);
if (parameters.stops) {
parameters.stops = parameters.stops.map((stop) => {
return [stop[0], parseFn(stop[1])];
});
}
if (parameters.default) {
parameters.default = parseFn(parameters.default);
}
else {
parameters.default = parseFn(propertySpec.default);
}
}
if (parameters.colorSpace && !isSupportedInterpolationColorSpace(parameters.colorSpace)) {
throw new Error(`Unknown color space: "${parameters.colorSpace}"`);
}
const innerFun = getInnerFunction(type);
let hashedStops;
let categoricalKeyType;
if (type === 'categorical') {
// For categorical functions, generate an Object as a hashmap of the stops for fast searching
hashedStops = Object.create(null);
for (const stop of parameters.stops) {
hashedStops[stop[0]] = stop[1];
}
// Infer key type based on first stop key-- used to encforce strict type checking later
categoricalKeyType = typeof parameters.stops[0][0];
}
if (zoomAndFeatureDependent) {
const featureFunctions = {};
const zoomStops = [];
for (let s = 0; s < parameters.stops.length; s++) {
const stop = parameters.stops[s];
const zoom = stop[0].zoom;
if (featureFunctions[zoom] === undefined) {
featureFunctions[zoom] = {
zoom,
type: parameters.type,
property: parameters.property,
default: parameters.default,
stops: []
};
zoomStops.push(zoom);
}
featureFunctions[zoom].stops.push([stop[0].value, stop[1]]);
}
const featureFunctionStops = [];
for (const z of zoomStops) {
featureFunctionStops.push([featureFunctions[z].zoom, createFunction(featureFunctions[z], propertySpec)]);
}
const interpolationType = { name: 'linear' };
return {
kind: 'composite',
interpolationType,
interpolationFactor: Interpolate.interpolationFactor.bind(undefined, interpolationType),
zoomStops: featureFunctionStops.map(s => s[0]),
evaluate({ zoom }, properties) {
return evaluateExponentialFunction({
stops: featureFunctionStops,
base: parameters.base
}, propertySpec, zoom).evaluate(zoom, properties);
}
};
}
else if (zoomDependent) {
const interpolationType = type === 'exponential' ?
{ name: 'exponential', base: parameters.base !== undefined ? parameters.base : 1 } : null;
return {
kind: 'camera',
interpolationType,
interpolationFactor: Interpolate.interpolationFactor.bind(undefined, interpolationType),
zoomStops: parameters.stops.map(s => s[0]),
evaluate: ({ zoom }) => innerFun(parameters, propertySpec, zoom, hashedStops, categoricalKeyType)
};
}
else {
return {
kind: 'source',
evaluate(_, feature) {
const value = feature && feature.properties ? feature.properties[parameters.property] : undefined;
if (value === undefined) {
return coalesce$1(parameters.default, propertySpec.default);
}
return innerFun(parameters, propertySpec, value, hashedStops, categoricalKeyType);
}
};
}
}
function coalesce$1(a, b, c) {
if (a !== undefined)
return a;
if (b !== undefined)
return b;
if (c !== undefined)
return c;
}
function evaluateCategoricalFunction(parameters, propertySpec, input, hashedStops, keyType) {
const evaluated = typeof input === keyType ? hashedStops[input] : undefined; // Enforce strict typing on input
return coalesce$1(evaluated, parameters.default, propertySpec.default);
}
function evaluateIntervalFunction(parameters, propertySpec, input) {
// Edge cases
if (getType(input) !== 'number')
return coalesce$1(parameters.default, propertySpec.default);
const n = parameters.stops.length;
if (n === 1)
return parameters.stops[0][1];
if (input <= parameters.stops[0][0])
return parameters.stops[0][1];
if (input >= parameters.stops[n - 1][0])
return parameters.stops[n - 1][1];
const index = findStopLessThanOrEqualTo(parameters.stops.map((stop) => stop[0]), input);
return parameters.stops[index][1];
}
function evaluateExponentialFunction(parameters, propertySpec, input) {
const base = parameters.base !== undefined ? parameters.base : 1;
// Edge cases
if (getType(input) !== 'number')
return coalesce$1(parameters.default, propertySpec.default);
const n = parameters.stops.length;
if (n === 1)
return parameters.stops[0][1];
if (input <= parameters.stops[0][0])
return parameters.stops[0][1];
if (input >= parameters.stops[n - 1][0])
return parameters.stops[n - 1][1];
const index = findStopLessThanOrEqualTo(parameters.stops.map((stop) => stop[0]), input);
const t = interpolationFactor(input, base, parameters.stops[index][0], parameters.stops[index + 1][0]);
const outputLower = parameters.stops[index][1];
const outputUpper = parameters.stops[index + 1][1];
const interp = interpolateFactory[propertySpec.type] || identityFunction;
if (typeof outputLower.evaluate === 'function') {
return {
evaluate(...args) {
const evaluatedLower = outputLower.evaluate.apply(undefined, args);
const evaluatedUpper = outputUpper.evaluate.apply(undefined, args);
// Special case for fill-outline-color, which has no spec default.
if (evaluatedLower === undefined || evaluatedUpper === undefined) {
return undefined;
}
return interp(evaluatedLower, evaluatedUpper, t, parameters.colorSpace);
}
};
}
return interp(outputLower, outputUpper, t, parameters.colorSpace);
}
function evaluateIdentityFunction(parameters, propertySpec, input) {
switch (propertySpec.type) {
case 'color':
input = Color.parse(input);
break;
case 'formatted':
input = Formatted.fromString(input.toString());
break;
case 'resolvedImage':
input = ResolvedImage.fromString(input.toString());
break;
case 'padding':
input = Padding.parse(input);
break;
case 'colorArray':
input = ColorArray.parse(input);
break;
case 'numberArray':
input = NumberArray.parse(input);
break;
default:
if (getType(input) !== propertySpec.type && (propertySpec.type !== 'enum' || !propertySpec.values[input])) {
input = undefined;
}
}
return coalesce$1(input, parameters.default, propertySpec.default);
}
/**
* Returns a ratio that can be used to interpolate between exponential function
* stops.
*
* 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,
* and `a` and `b` are constants affording sufficient degrees of freedom to fit
* the function to the given stops.
*
* Here's a bit of algebra that lets us compute `f(x)` directly from the stop
* values without explicitly solving for `a` and `b`:
*
* First stop value: `f(x0) = y0 = a * base^x0 + b`
* Second stop value: `f(x1) = y1 = a * base^x1 + b`
* => `y1 - y0 = a(base^x1 - base^x0)`
* => `a = (y1 - y0)/(base^x1 - base^x0)`
*
* Desired value: `f(x) = y = a * base^x + b`
* => `f(x) = y0 + a * (base^x - base^x0)`
*
* From the above, we can replace the `a` in `a * (base^x - base^x0)` and do a
* little algebra:
* ```
* a * (base^x - base^x0) = (y1 - y0)/(base^x1 - base^x0) * (base^x - base^x0)
* = (y1 - y0) * (base^x - base^x0) / (base^x1 - base^x0)
* ```
*
* If we let `(base^x - base^x0) / (base^x1 base^x0)`, then we have
* `f(x) = y0 + (y1 - y0) * ratio`. In other words, `ratio` may be treated as
* an interpolation factor between the two stops' output values.
*
* (Note: a slightly different form for `ratio`,
* `(base^(x-x0) - 1) / (base^(x1-x0) - 1) `, is equivalent, but requires fewer
* expensive `Math.pow()` operations.)
*
* @private
*/
function interpolationFactor(input, base, lowerValue, upperValue) {
const difference = upperValue - lowerValue;
const progress = input - lowerValue;
if (difference === 0) {
return 0;
}
else if (base === 1) {
return progress / difference;
}
else {
return (Math.pow(base, progress) - 1) / (Math.pow(base, difference) - 1);
}
}
class StyleExpression {
constructor(expression, propertySpec, globalState) {
this.expression = expression;
this._warningHistory = {};
this._evaluator = new EvaluationContext();
this._defaultValue = propertySpec ? getDefaultValue(propertySpec) : null;
this._enumValues = propertySpec && propertySpec.type === 'enum' ? propertySpec.values : null;
this._globalState = globalState;
}
evaluateWithoutErrorHandling(globals, feature, featureState, canonical, availableImages, formattedSection) {
if (this._globalState) {
globals = addGlobalState(globals, this._globalState);
}
this._evaluator.globals = globals;
this._evaluator.feature = feature;
this._evaluator.featureState = featureState;
this._evaluator.canonical = canonical;
this._evaluator.availableImages = availableImages || null;
this._evaluator.formattedSection = formattedSection;
return this.expression.evaluate(this._evaluator);
}
evaluate(globals, feature, featureState, canonical, availableImages, formattedSection) {
if (this._globalState) {
globals = addGlobalState(globals, this._globalState);
}
this._evaluator.globals = globals;
this._evaluator.feature = feature || null;
this._evaluator.featureState = featureState || null;
this._evaluator.canonical = canonical;
this._evaluator.availableImages = availableImages || null;
this._evaluator.formattedSection = formattedSection || null;
try {
const val = this.expression.evaluate(this._evaluator);
if (val === null || val === undefined || (typeof val === 'number' && val !== val)) {
return this._defaultValue;
}
if (this._enumValues && !(val in this._enumValues)) {
throw new RuntimeError(`Expected value to be one of ${Object.keys(this._enumValues).map(v => JSON.stringify(v)).join(', ')}, but found ${JSON.stringify(val)} instead.`);
}
return val;
}
catch (e) {
if (!this._warningHistory[e.message]) {
this._warningHistory[e.message] = true;
if (typeof console !== 'undefined') {
console.warn(e.message);
}
}
return this._defaultValue;
}
}
}
function isExpression(expression) {
return Array.isArray(expression) && expression.length > 0 &&
typeof expression[0] === 'string' && expression[0] in expressions$1;
}
/**
* Parse and typecheck the given style spec JSON expression. If
* options.defaultValue is provided, then the resulting StyleExpression's
* `evaluate()` method will handle errors by logging a warning (once per
* message) and returning the default value. Otherwise, it will throw
* evaluation errors.
*
* @private
*/
function createExpression(expression, propertySpec, globalState) {
const parser = new ParsingContext(expressions$1, isExpressionConstant, [], propertySpec ? getExpectedType(propertySpec) : undefined);
// For string-valued properties, coerce to string at the top level rather than asserting.
const parsed = parser.parse(expression, undefined, undefined, undefined, propertySpec && propertySpec.type === 'string' ? { typeAnnotation: 'coerce' } : undefined);
if (!parsed) {
return error(parser.errors);
}
return success(new StyleExpression(parsed, propertySpec, globalState));
}
class ZoomConstantExpression {
constructor(kind, expression, globalState) {
this.kind = kind;
this._styleExpression = expression;
this.isStateDependent = kind !== 'constant' && !isStateConstant(expression.expression);
this.globalStateRefs = findGlobalStateRefs(expression.expression);
this._globalState = globalState;
}
evaluateWithoutErrorHandling(globals, feature, featureState, canonical, availableImages, formattedSection) {
if (this._globalState) {
globals = addGlobalState(globals, this._globalState);
}
return this._styleExpression.evaluateWithoutErrorHandling(globals, feature, featureState, canonical, availableImages, formattedSection);
}
evaluate(globals, feature, featureState, canonical, availableImages, formattedSection) {
if (this._globalState) {
globals = addGlobalState(globals, this._globalState);
}
return this._styleExpression.evaluate(globals, feature, featureState, canonical, availableImages, formattedSection);
}
}
class ZoomDependentExpression {
constructor(kind, expression, zoomStops, interpolationType, globalState) {
this.kind = kind;
this.zoomStops = zoomStops;
this._styleExpression = expression;
this.isStateDependent = kind !== 'camera' && !isStateConstant(expression.expression);
this.globalStateRefs = findGlobalStateRefs(expression.expression);
this.interpolationType = interpolationType;
this._globalState = globalState;
}
evaluateWithoutErrorHandling(globals, feature, featureState, canonical, availableImages, formattedSection) {
if (this._globalState) {
globals = addGlobalState(globals, this._globalState);
}
return this._styleExpression.evaluateWithoutErrorHandling(globals, feature, featureState, canonical, availableImages, formattedSection);
}
evaluate(globals, feature, featureState, canonical, availableImages, formattedSection) {
if (this._globalState) {
globals = addGlobalState(globals, this._globalState);
}
return this._styleExpression.evaluate(globals, feature, featureState, canonical, availableImages, formattedSection);
}
interpolationFactor(input, lower, upper) {
if (this.interpolationType) {
return Interpolate.interpolationFactor(this.interpolationType, input, lower, upper);
}
else {
return 0;
}
}
}
function isZoomExpression(expression) {
return expression._styleExpression !== undefined;
}
function createPropertyExpression(expressionInput, propertySpec, globalState) {
const expression = createExpression(expressionInput, propertySpec, globalState);
if (expression.result === 'error') {
return expression;
}
const parsed = expression.value.expression;
const isFeatureConstantResult = isFeatureConstant(parsed);
if (!isFeatureConstantResult && !supportsPropertyExpression(propertySpec)) {
return error([new ExpressionParsingError('', 'data expressions not supported')]);
}
const isZoomConstant = isGlobalPropertyConstant(parsed, ['zoom']);
if (!isZoomConstant && !supportsZoomExpression(propertySpec)) {
return error([new ExpressionParsingError('', 'zoom expressions not supported')]);
}
const zoomCurve = findZoomCurve(parsed);
if (!zoomCurve && !isZoomConstant) {
return error([new ExpressionParsingError('', '"zoom" expression may only be used as input to a top-level "step" or "interpolate" expression.')]);
}
else if (zoomCurve instanceof ExpressionParsingError) {
return error([zoomCurve]);
}
else if (zoomCurve instanceof Interpolate && !supportsInterpolation(propertySpec)) {
return error([new ExpressionParsingError('', '"interpolate" expressions cannot be used with this property')]);
}
if (!zoomCurve) {
return success(isFeatureConstantResult ?
new ZoomConstantExpression('constant', expression.value, globalState) :
new ZoomConstantExpression('source', expression.value, globalState));
}
const interpolationType = zoomCurve instanceof Interpolate ? zoomCurve.interpolation : undefined;
return success(isFeatureConstantResult ?
new ZoomDependentExpression('camera', expression.value, zoomCurve.labels, interpolationType, globalState) :
new ZoomDependentExpression('composite', expression.value, zoomCurve.labels, interpolationType, globalState));
}
// serialization wrapper for old-style stop functions normalized to the
// expression interface
class StylePropertyFunction {
constructor(parameters, specification) {
this._parameters = parameters;
this._specification = specification;
extendBy(this, createFunction(this._parameters, this._specification));
}
static deserialize(serialized) {
return new StylePropertyFunction(serialized._parameters, serialized._specification);
}
static serialize(input) {
return {
_parameters: input._parameters,
_specification: input._specification
};
}
}
function normalizePropertyExpression(value, specification, globalState) {
if (isFunction$1(value)) {
return new StylePropertyFunction(value, specification);
}
else if (isExpression(value)) {
const expression = createPropertyExpression(value, specification, globalState);
if (expression.result === 'error') {
// this should have been caught in validation
throw new Error(expression.value.map(err => `${err.key}: ${err.message}`).join(', '));
}
return expression.value;
}
else {
let constant = value;
if (specification.type === 'color' && typeof value === 'string') {
constant = Color.parse(value);
}
else if (specification.type === 'padding' && (typeof value === 'number' || Array.isArray(value))) {
constant = Padding.parse(value);
}
else if (specification.type === 'numberArray' && (typeof value === 'number' || Array.isArray(value))) {
constant = NumberArray.parse(value);
}
else if (specification.type === 'colorArray' && (typeof value === 'string' || Array.isArray(value))) {
constant = ColorArray.parse(value);
}
else if (specification.type === 'variableAnchorOffsetCollection' && Array.isArray(value)) {
constant = VariableAnchorOffsetCollection.parse(value);
}
else if (specification.type === 'projectionDefinition' && typeof value === 'string') {
constant = ProjectionDefinition.parse(value);
}
return {
globalStateRefs: new Set(),
_globalState: null,
kind: 'constant',
evaluate: () => constant
};
}
}
// Zoom-dependent expressions may only use ["zoom"] as the input to a top-level "step" or "interpolate"
// expression (collectively referred to as a "curve"). The curve may be wrapped in one or more "let" or
// "coalesce" expressions.
function findZoomCurve(expression) {
let result = null;
if (expression instanceof Let) {
result = findZoomCurve(expression.result);
}
else if (expression instanceof Coalesce) {
for (const arg of expression.args) {
result = findZoomCurve(arg);
if (result) {
break;
}
}
}
else if ((expression instanceof Step || expression instanceof Interpolate) &&
expression.input instanceof CompoundExpression &&
expression.input.name === 'zoom') {
result = expression;
}
if (result instanceof ExpressionParsingError) {
return result;
}
expression.eachChild((child) => {
const childResult = findZoomCurve(child);
if (childResult instanceof ExpressionParsingError) {
result = childResult;
}
else if (!result && childResult) {
result = new ExpressionParsingError('', '"zoom" expression may only be used as input to a top-level "step" or "interpolate" expression.');
}
else if (result && childResult && result !== childResult) {
result = new ExpressionParsingError('', 'Only one zoom-based "step" or "interpolate" subexpression may be used in an expression.');
}
});
return result;
}
function findGlobalStateRefs(expression, results = new Set()) {
if (expression instanceof GlobalState) {
results.add(expression.key);
}
expression.eachChild(childExpression => {
findGlobalStateRefs(childExpression, results);
});
return results;
}
function getExpectedType(spec) {
const types = {
color: ColorType,
string: StringType,
number: NumberType,
enum: StringType,
boolean: BooleanType,
formatted: FormattedType,
padding: PaddingType,
numberArray: NumberArrayType,
colorArray: ColorArrayType,
projectionDefinition: ProjectionDefinitionType,
resolvedImage: ResolvedImageType,
variableAnchorOffsetCollection: VariableAnchorOffsetCollectionType
};
if (spec.type === 'array') {
return array(types[spec.value] || ValueType, spec.length);
}
return types[spec.type];
}
function getDefaultValue(spec) {
if (spec.type === 'color' && isFunction$1(spec.default)) {
// Special case for heatmap-color: it uses the 'default:' to define a
// default color ramp, but createExpression expects a simple value to fall
// back to in case of runtime errors
return new Color(0, 0, 0, 0);
}
switch (spec.type) {
case 'color':
return Color.parse(spec.default) || null;
case 'padding':
return Padding.parse(spec.default) || null;
case 'numberArray':
return NumberArray.parse(spec.default) || null;
case 'colorArray':
return ColorArray.parse(spec.default) || null;
case 'variableAnchorOffsetCollection':
return VariableAnchorOffsetCollection.parse(spec.default) || null;
case 'projectionDefinition':
return ProjectionDefinition.parse(spec.default) || null;
default:
return (spec.default === undefined ? null : spec.default);
}
}
function addGlobalState(globals, globalState) {
const { zoom, heatmapDensity, elevation, lineProgress, isSupportedScript, accumulated } = globals !== null && globals !== void 0 ? globals : {};
return {
zoom,
heatmapDensity,
elevation,
lineProgress,
isSupportedScript,
accumulated,
globalState
};
}
function isExpressionFilter(filter) {
if (filter === true || filter === false) {
return true;
}
if (!Array.isArray(filter) || filter.length === 0) {
return false;
}
switch (filter[0]) {
case 'has':
return filter.length >= 2 && filter[1] !== '$id' && filter[1] !== '$type';
case 'in':
return filter.length >= 3 && (typeof filter[1] !== 'string' || Array.isArray(filter[2]));
case '!in':
case '!has':
case 'none':
return false;
case '==':
case '!=':
case '>':
case '>=':
case '<':
case '<=':
return filter.length !== 3 || (Array.isArray(filter[1]) || Array.isArray(filter[2]));
case 'any':
case 'all':
for (const f of filter.slice(1)) {
if (!isExpressionFilter(f) && typeof f !== 'boolean') {
return false;
}
}
return true;
default:
return true;
}
}
const filterSpec = {
'type': 'boolean',
'default': false,
'transition': false,
'property-type': 'data-driven',
'expression': {
'interpolated': false,
'parameters': ['zoom', 'feature']
}
};
/**
* Given a filter expressed as nested arrays, return a new function
* that evaluates whether a given feature (with a .properties or .tags property)
* passes its test.
*
* @private
* @param filter MapLibre filter
* @param [globalState] Global state object to be used for evaluating 'global-state' expressions
* @returns filter-evaluating function
*/
function featureFilter(filter, globalState) {
if (filter === null || filter === undefined) {
return { filter: () => true, needGeometry: false, getGlobalStateRefs: () => new Set() };
}
if (!isExpressionFilter(filter)) {
filter = convertFilter$1(filter);
}
const compiled = createExpression(filter, filterSpec, globalState);
if (compiled.result === 'error') {
throw new Error(compiled.value.map(err => `${err.key}: ${err.message}`).join(', '));
}
else {
const needGeometry = geometryNeeded(filter);
return {
filter: (globalProperties, feature, canonical) => compiled.value.evaluate(globalProperties, feature, {}, canonical),
needGeometry,
getGlobalStateRefs: () => findGlobalStateRefs(compiled.value.expression)
};
}
}
// Comparison function to sort numbers and strings
function compare(a, b) {
return a < b ? -1 : a > b ? 1 : 0;
}
function geometryNeeded(filter) {
if (!Array.isArray(filter))
return false;
if (filter[0] === 'within' || filter[0] === 'distance')
return true;
for (let index = 1; index < filter.length; index++) {
if (geometryNeeded(filter[index]))
return true;
}
return false;
}
function convertFilter$1(filter) {
if (!filter)
return true;
const op = filter[0];
if (filter.length <= 1)
return (op !== 'any');
const converted = op === '==' ? convertComparisonOp$1(filter[1], filter[2], '==') :
op === '!=' ? convertNegation(convertComparisonOp$1(filter[1], filter[2], '==')) :
op === '<' ||
op === '>' ||
op === '<=' ||
op === '>=' ? convertComparisonOp$1(filter[1], filter[2], op) :
op === 'any' ? convertDisjunctionOp(filter.slice(1)) :
op === 'all' ? ['all'].concat(filter.slice(1).map(convertFilter$1)) :
op === 'none' ? ['all'].concat(filter.slice(1).map(convertFilter$1).map(convertNegation)) :
op === 'in' ? convertInOp$1(filter[1], filter.slice(2)) :
op === '!in' ? convertNegation(convertInOp$1(filter[1], filter.slice(2))) :
op === 'has' ? convertHasOp$1(filter[1]) :
op === '!has' ? convertNegation(convertHasOp$1(filter[1])) :
true;
return converted;
}
function convertComparisonOp$1(property, value, op) {
switch (property) {
case '$type':
return [`filter-type-${op}`, value];
case '$id':
return [`filter-id-${op}`, value];
default:
return [`filter-${op}`, property, value];
}
}
function convertDisjunctionOp(filters) {
return ['any'].concat(filters.map(convertFilter$1));
}
function convertInOp$1(property, values) {
if (values.length === 0) {
return false;
}
switch (property) {
case '$type':
return ['filter-type-in', ['literal', values]];
case '$id':
return ['filter-id-in', ['literal', values]];
default:
if (values.length > 200 && !values.some(v => typeof v !== typeof values[0])) {
return ['filter-in-large', property, ['literal', values.sort(compare)]];
}
else {
return ['filter-in-small', property, ['literal', values]];
}
}
}
function convertHasOp$1(property) {
switch (property) {
case '$type':
return true;
case '$id':
return ['filter-has-id'];
default:
return ['filter-has', property];
}
}
function convertNegation(filter) {
return ['!', filter];
}
/*
* Convert the given filter to an expression, storing the expected types for
* any feature properties referenced in expectedTypes.
*
* These expected types are needed in order to construct preflight type checks
* needed for handling 'any' filters. A preflight type check is necessary in
* order to mimic legacy filters' semantics around expected type mismatches.
* For example, consider the legacy filter:
*
* ["any", ["all", [">", "y", 0], [">", "y", 0]], [">", "x", 0]]
*
* Naively, we might convert this to the expression:
*
* ["any", ["all", [">", ["get", "y"], 0], [">", ["get", "z"], 0]], [">", ["get", "x"], 0]]
*
* But if we tried to evaluate this against, say `{x: 1, y: null, z: 0}`, the
* [">", ["get", "y"], 0] would cause an evaluation error, leading to the
* entire filter returning false. Legacy filter semantics, though, ask for
* [">", "y", 0] to simply return `false` when `y` is of the wrong type,
* allowing the subsequent terms of the outer "any" expression to be evaluated
* (resulting, in this case, in a `true` value, because x > 0).
*
* We account for this by inserting a preflight type-checking expression before
* each "any" term, allowing us to avoid evaluating the actual converted filter
* if any type mismatches would cause it to produce an evaluation error:
*
* ["any",
* ["case",
* ["all", ["==", ["typeof", ["get", "y"]], "number"], ["==", ["typeof", ["get", "z"], "number]],
* ["all", [">", ["get", "y"], 0], [">", ["get", "z"], 0]],
* false
* ],
* ["case",
* ["==", ["typeof", ["get", "x"], "number"]],
* [">", ["get", "x"], 0],
* false
* ]
* ]
*
* An alternative, possibly more direct approach would be to use type checks
* in the conversion of each comparison operator, so that the converted version
* of each individual ==, >=, etc. would mimic the legacy filter semantics. The
* downside of this approach is that it can lead to many more type checks than
* would otherwise be necessary: outside the context of an "any" expression,
* bailing out due to a runtime type error (expression semantics) and returning
* false (legacy filter semantics) are equivalent: they cause the filter to
* produce a `false` result.
*/
function convertFilter(filter, expectedTypes = {}) {
if (isExpressionFilter(filter))
return filter;
if (!filter)
return true;
const legacyFilter = filter;
const legacyOp = legacyFilter[0];
if (filter.length <= 1)
return (legacyOp !== 'any');
switch (legacyOp) {
case '==':
case '!=':
case '<':
case '>':
case '<=':
case '>=': {
const [, property, value] = filter;
return convertComparisonOp(property, value, legacyOp, expectedTypes);
}
case 'any': {
const [, ...conditions] = legacyFilter;
const children = conditions.map((f) => {
const types = {};
const child = convertFilter(f, types);
const typechecks = runtimeTypeChecks(types);
return typechecks === true ? child : ['case', typechecks, child, false];
});
return ['any', ...children];
}
case 'all': {
const [, ...conditions] = legacyFilter;
const children = conditions.map(f => convertFilter(f, expectedTypes));
return children.length > 1 ? ['all', ...children] : children[0];
}
case 'none': {
const [, ...conditions] = legacyFilter;
return ['!', convertFilter(['any', ...conditions], {})];
}
case 'in': {
const [, property, ...values] = legacyFilter;
return convertInOp(property, values);
}
case '!in': {
const [, property, ...values] = legacyFilter;
return convertInOp(property, values, true);
}
case 'has':
return convertHasOp(legacyFilter[1]);
case '!has':
return ['!', convertHasOp(legacyFilter[1])];
default:
return true;
}
}
// Given a set of feature properties and an expected type for each one,
// construct an boolean expression that tests whether each property has the
// right type.
// E.g.: for {name: 'string', population: 'number'}, return
// [ 'all',
// ['==', ['typeof', ['get', 'name'], 'string']],
// ['==', ['typeof', ['get', 'population'], 'number]]
// ]
function runtimeTypeChecks(expectedTypes) {
const conditions = [];
for (const property in expectedTypes) {
const get = property === '$id' ? ['id'] : ['get', property];
conditions.push(['==', ['typeof', get], expectedTypes[property]]);
}
if (conditions.length === 0)
return true;
if (conditions.length === 1)
return conditions[0];
return ['all', ...conditions];
}
function convertComparisonOp(property, value, op, expectedTypes) {
let get;
if (property === '$type') {
return [op, ['geometry-type'], value];
}
else if (property === '$id') {
get = ['id'];
}
else {
get = ['get', property];
}
if (expectedTypes && value !== null) {
const type = typeof value;
expectedTypes[property] = type;
}
if (op === '==' && property !== '$id' && value === null) {
return [
'all',
['has', property], // missing property != null for legacy filters
['==', get, null]
];
}
else if (op === '!=' && property !== '$id' && value === null) {
return [
'any',
['!', ['has', property]], // missing property != null for legacy filters
['!=', get, null]
];
}
return [op, get, value];
}
function convertInOp(property, values, negate = false) {
if (values.length === 0)
return negate;
let get;
if (property === '$type') {
get = ['geometry-type'];
}
else if (property === '$id') {
get = ['id'];
}
else {
get = ['get', property];
}
// Determine if the list of values to be searched is homogenously typed.
// If so (and if the type is string or number), then we can use a
// [match, input, [...values], true, false] construction rather than a
// bunch of `==` tests.
let uniformTypes = true;
const type = typeof values[0];
for (const value of values) {
if (typeof value !== type) {
uniformTypes = false;
break;
}
}
if (uniformTypes && (type === 'string' || type === 'number')) {
// Match expressions must have unique values.
const uniqueValues = values.sort().filter((v, i) => i === 0 || values[i - 1] !== v);
return ['match', get, uniqueValues, !negate, negate];
}
if (negate) {
return ['all', ...values.map(v => ['!=', get, v])];
}
else {
return ['any', ...values.map(v => ['==', get, v])];
}
}
function convertHasOp(property) {
if (property === '$type') {
return true;
}
else if (property === '$id') {
return ['!=', ['id'], null];
}
else {
return ['has', property];
}
}
function convertLiteral(value) {
return typeof value === 'object' ? ['literal', value] : value;
}
function convertFunction(parameters, propertySpec) {
let stops = parameters.stops;
if (!stops) {
// identity function
return convertIdentityFunction(parameters, propertySpec);
}
const zoomAndFeatureDependent = stops && typeof stops[0][0] === 'object';
const featureDependent = zoomAndFeatureDependent || parameters.property !== undefined;
const zoomDependent = zoomAndFeatureDependent || !featureDependent;
stops = stops.map((stop) => {
if (!featureDependent && propertySpec.tokens && typeof stop[1] === 'string') {
return [stop[0], convertTokenString(stop[1])];
}
return [stop[0], convertLiteral(stop[1])];
});
if (zoomAndFeatureDependent) {
return convertZoomAndPropertyFunction(parameters, propertySpec, stops);
}
else if (zoomDependent) {
return convertZoomFunction(parameters, propertySpec, stops);
}
else {
return convertPropertyFunction(parameters, propertySpec, stops);
}
}
function convertIdentityFunction(parameters, propertySpec) {
const get = ['get', parameters.property];
if (parameters.default === undefined) {
// By default, expressions for string-valued properties get coerced. To preserve
// legacy function semantics, insert an explicit assertion instead.
return propertySpec.type === 'string' ? ['string', get] : get;
}
else if (propertySpec.type === 'enum') {
return [
'match',
get,
Object.keys(propertySpec.values),
get,
parameters.default
];
}
else {
const expression = [propertySpec.type === 'color' ? 'to-color' : propertySpec.type, get, convertLiteral(parameters.default)];
if (propertySpec.type === 'array') {
expression.splice(1, 0, propertySpec.value, propertySpec.length || null);
}
return expression;
}
}
function getInterpolateOperator(parameters) {
switch (parameters.colorSpace) {
case 'hcl': return 'interpolate-hcl';
case 'lab': return 'interpolate-lab';
default: return 'interpolate';
}
}
function convertZoomAndPropertyFunction(parameters, propertySpec, stops) {
const featureFunctionParameters = {};
const featureFunctionStops = {};
const zoomStops = [];
for (let s = 0; s < stops.length; s++) {
const stop = stops[s];
const zoom = stop[0].zoom;
if (featureFunctionParameters[zoom] === undefined) {
featureFunctionParameters[zoom] = {
zoom,
type: parameters.type,
property: parameters.property,
default: parameters.default,
};
featureFunctionStops[zoom] = [];
zoomStops.push(zoom);
}
featureFunctionStops[zoom].push([stop[0].value, stop[1]]);
}
// the interpolation type for the zoom dimension of a zoom-and-property
// function is determined directly from the style property specification
// for which it's being used: linear for interpolatable properties, step
// otherwise.
const functionType = getFunctionType({}, propertySpec);
if (functionType === 'exponential') {
const expression = [getInterpolateOperator(parameters), ['linear'], ['zoom']];
for (const z of zoomStops) {
const output = convertPropertyFunction(featureFunctionParameters[z], propertySpec, featureFunctionStops[z]);
appendStopPair(expression, z, output, false);
}
return expression;
}
else {
const expression = ['step', ['zoom']];
for (const z of zoomStops) {
const output = convertPropertyFunction(featureFunctionParameters[z], propertySpec, featureFunctionStops[z]);
appendStopPair(expression, z, output, true);
}
fixupDegenerateStepCurve(expression);
return expression;
}
}
function coalesce(a, b) {
if (a !== undefined)
return a;
if (b !== undefined)
return b;
}
function getFallback(parameters, propertySpec) {
const defaultValue = convertLiteral(coalesce(parameters.default, propertySpec.default));
/*
* Some fields with type: resolvedImage have an undefined default.
* Because undefined is an invalid value for resolvedImage, set fallback to
* an empty string instead of undefined to ensure output
* passes validation.
*/
if (defaultValue === undefined && propertySpec.type === 'resolvedImage') {
return '';
}
return defaultValue;
}
function convertPropertyFunction(parameters, propertySpec, stops) {
const type = getFunctionType(parameters, propertySpec);
const get = ['get', parameters.property];
if (type === 'categorical' && typeof stops[0][0] === 'boolean') {
const expression = ['case'];
for (const stop of stops) {
expression.push(['==', get, stop[0]], stop[1]);
}
expression.push(getFallback(parameters, propertySpec));
return expression;
}
else if (type === 'categorical') {
const expression = ['match', get];
for (const stop of stops) {
appendStopPair(expression, stop[0], stop[1], false);
}
expression.push(getFallback(parameters, propertySpec));
return expression;
}
else if (type === 'interval') {
const expression = ['step', ['number', get]];
for (const stop of stops) {
appendStopPair(expression, stop[0], stop[1], true);
}
fixupDegenerateStepCurve(expression);
return parameters.default === undefined ? expression : [
'case',
['==', ['typeof', get], 'number'],
expression,
convertLiteral(parameters.default)
];
}
else if (type === 'exponential') {
const base = parameters.base !== undefined ? parameters.base : 1;
const expression = [
getInterpolateOperator(parameters),
base === 1 ? ['linear'] : ['exponential', base],
['number', get]
];
for (const stop of stops) {
appendStopPair(expression, stop[0], stop[1], false);
}
return parameters.default === undefined ? expression : [
'case',
['==', ['typeof', get], 'number'],
expression,
convertLiteral(parameters.default)
];
}
else {
throw new Error(`Unknown property function type ${type}`);
}
}
function convertZoomFunction(parameters, propertySpec, stops, input = ['zoom']) {
const type = getFunctionType(parameters, propertySpec);
let expression;
let isStep = false;
if (type === 'interval') {
expression = ['step', input];
isStep = true;
}
else if (type === 'exponential') {
const base = parameters.base !== undefined ? parameters.base : 1;
expression = [getInterpolateOperator(parameters), base === 1 ? ['linear'] : ['exponential', base], input];
}
else {
throw new Error(`Unknown zoom function type "${type}"`);
}
for (const stop of stops) {
appendStopPair(expression, stop[0], stop[1], isStep);
}
fixupDegenerateStepCurve(expression);
return expression;
}
function fixupDegenerateStepCurve(expression) {
// degenerate step curve (i.e. a constant function): add a noop stop
if (expression[0] === 'step' && expression.length === 3) {
expression.push(0);
expression.push(expression[3]);
}
}
function appendStopPair(curve, input, output, isStep) {
// Skip duplicate stop values. They were not validated for functions, but they are for expressions.
// https://github.com/mapbox/mapbox-gl-js/issues/4107
if (curve.length > 3 && input === curve[curve.length - 2]) {
return;
}
// step curves don't get the first input value, as it is redundant.
if (!(isStep && curve.length === 2)) {
curve.push(input);
}
curve.push(output);
}
function getFunctionType(parameters, propertySpec) {
if (parameters.type) {
return parameters.type;
}
else {
return propertySpec.expression.interpolated ? 'exponential' : 'interval';
}
}
// "String with {name} token" => ["concat", "String with ", ["get", "name"], " token"]
function convertTokenString(s) {
const result = ['concat'];
const re = /{([^{}]+)}/g;
let pos = 0;
for (let match = re.exec(s); match !== null; match = re.exec(s)) {
const literal = s.slice(pos, re.lastIndex - match[0].length);
pos = re.lastIndex;
if (literal.length > 0)
result.push(literal);
result.push(['get', match[1]]);
}
if (result.length === 1) {
return s;
}
if (pos < s.length) {
result.push(s.slice(pos));
}
else if (result.length === 2) {
return ['to-string', result[1]];
}
return result;
}
function getPropertyReference(propertyName) {
for (let i = 0; i < v8Spec.layout.length; i++) {
for (const key in v8Spec[v8Spec.layout[i]]) {
if (key === propertyName)
return v8Spec[v8Spec.layout[i]][key];
}
}
for (let i = 0; i < v8Spec.paint.length; i++) {
for (const key in v8Spec[v8Spec.paint[i]]) {
if (key === propertyName)
return v8Spec[v8Spec.paint[i]][key];
}
}
return null;
}
function eachSource(style, callback) {
for (const k in style.sources) {
callback(style.sources[k]);
}
}
function eachLayer(style, callback) {
for (const layer of style.layers) {
callback(layer);
}
}
function eachProperty(style, options, callback) {
function inner(layer, propertyType) {
const properties = layer[propertyType];
if (!properties)
return;
Object.keys(properties).forEach((key) => {
callback({
path: [layer.id, propertyType, key],
key,
value: properties[key],
reference: getPropertyReference(key),
set(x) {
properties[key] = x;
}
});
});
}
eachLayer(style, (layer) => {
if (options.paint) {
inner(layer, 'paint');
}
if (options.layout) {
inner(layer, 'layout');
}
});
}
function stringify$1(obj) {
const type = typeof obj;
if (type === 'number' || type === 'boolean' || type === 'string' || obj === undefined || obj === null)
return JSON.stringify(obj);
if (Array.isArray(obj)) {
let str = '[';
for (const val of obj) {
str += `${stringify$1(val)},`;
}
return `${str}]`;
}
const keys = Object.keys(obj).sort();
let str = '{';
for (let i = 0; i < keys.length; i++) {
str += `${JSON.stringify(keys[i])}:${stringify$1(obj[keys[i]])},`;
}
return `${str}}`;
}
function getKey(layer) {
let key = '';
for (const k of refProperties) {
key += `/${stringify$1(layer[k])}`;
}
return key;
}
/**
* Groups layers by their layout-affecting properties.
* These are the properties that were formerly used by explicit `ref` mechanism
* for layers: 'type', 'source', 'source-layer', 'minzoom', 'maxzoom',
* 'filter', and 'layout'.
*
* The input is not modified. The output layers are references to the
* input layers.
*
* @param layers - an array of {@link LayerSpecification}.
* @param cachedKeys - an object to keep already calculated keys.
* @returns an array of arrays of {@link LayerSpecification} objects, where each inner array
* contains layers that share the same layout-affecting properties.
*/
function groupByLayout(layers, cachedKeys) {
const groups = {};
for (let i = 0; i < layers.length; i++) {
const k = (cachedKeys && cachedKeys[layers[i].id]) || getKey(layers[i]);
// update the cache if there is one
if (cachedKeys)
cachedKeys[layers[i].id] = k;
let group = groups[k];
if (!group) {
group = groups[k] = [];
}
group.push(layers[i]);
}
const result = [];
for (const k in groups) {
result.push(groups[k]);
}
return result;
}
function emptyStyle() {
const style = {};
const version = v8Spec['$version'];
for (const styleKey in v8Spec['$root']) {
const specification = v8Spec['$root'][styleKey];
if (specification.required) {
let value = null;
if (styleKey === 'version') {
value = version;
}
else {
if (specification.type === 'array') {
value = [];
}
else {
value = {};
}
}
if (value != null) {
style[styleKey] = value;
}
}
}
return style;
}
function validateConstants(options) {
const key = options.key;
const constants = options.value;
if (constants) {
return [new ValidationError(key, constants, 'constants have been deprecated as of v8')];
}
else {
return [];
}
}
// Turn jsonlint-lines-primitives objects into primitive objects
function unbundle(value) {
if (value instanceof Number || value instanceof String || value instanceof Boolean) {
return value.valueOf();
}
else {
return value;
}
}
function deepUnbundle(value) {
if (Array.isArray(value)) {
return value.map(deepUnbundle);
}
else if (value instanceof Object && !(value instanceof Number || value instanceof String || value instanceof Boolean)) {
const unbundledValue = {};
for (const key in value) {
unbundledValue[key] = deepUnbundle(value[key]);
}
return unbundledValue;
}
return unbundle(value);
}
function validateObject(options) {
const key = options.key;
const object = options.value;
const elementSpecs = options.valueSpec || {};
const elementValidators = options.objectElementValidators || {};
const style = options.style;
const styleSpec = options.styleSpec;
const validateSpec = options.validateSpec;
let errors = [];
const type = getType(object);
if (type !== 'object') {
return [new ValidationError(key, object, `object expected, ${type} found`)];
}
for (const objectKey in object) {
const elementSpecKey = objectKey.split('.')[0]; // treat 'paint.*' as 'paint'
// objectKey comes from the user controlled style input, so elementSpecKey may be e.g. "__proto__"
const elementSpec = getOwn(elementSpecs, elementSpecKey) || elementSpecs['*'];
let validateElement;
if (getOwn(elementValidators, elementSpecKey)) {
validateElement = elementValidators[elementSpecKey];
}
else if (getOwn(elementSpecs, elementSpecKey)) {
validateElement = validateSpec;
}
else if (elementValidators['*']) {
validateElement = elementValidators['*'];
}
else if (elementSpecs['*']) {
validateElement = validateSpec;
}
else {
errors.push(new ValidationError(key, object[objectKey], `unknown property "${objectKey}"`));
continue;
}
errors = errors.concat(validateElement({
key: (key ? `${key}.` : key) + objectKey,
value: object[objectKey],
valueSpec: elementSpec,
style,
styleSpec,
object,
objectKey,
validateSpec,
}, object));
}
for (const elementSpecKey in elementSpecs) {
// Don't check `required` when there's a custom validator for that property.
if (elementValidators[elementSpecKey]) {
continue;
}
if (elementSpecs[elementSpecKey].required && elementSpecs[elementSpecKey]['default'] === undefined && object[elementSpecKey] === undefined) {
errors.push(new ValidationError(key, object, `missing required property "${elementSpecKey}"`));
}
}
return errors;
}
function validateArray(options) {
const array = options.value;
const arraySpec = options.valueSpec;
const validateSpec = options.validateSpec;
const style = options.style;
const styleSpec = options.styleSpec;
const key = options.key;
const validateArrayElement = options.arrayElementValidator || validateSpec;
if (getType(array) !== 'array') {
return [new ValidationError(key, array, `array expected, ${getType(array)} found`)];
}
if (arraySpec.length && array.length !== arraySpec.length) {
return [new ValidationError(key, array, `array length ${arraySpec.length} expected, length ${array.length} found`)];
}
if (arraySpec['min-length'] && array.length < arraySpec['min-length']) {
return [new ValidationError(key, array, `array length at least ${arraySpec['min-length']} expected, length ${array.length} found`)];
}
let arrayElementSpec = {
'type': arraySpec.value,
'values': arraySpec.values
};
if (styleSpec.$version < 7) {
arrayElementSpec['function'] = arraySpec.function;
}
if (getType(arraySpec.value) === 'object') {
arrayElementSpec = arraySpec.value;
}
let errors = [];
for (let i = 0; i < array.length; i++) {
errors = errors.concat(validateArrayElement({
array,
arrayIndex: i,
value: array[i],
valueSpec: arrayElementSpec,
validateSpec: options.validateSpec,
style,
styleSpec,
key: `${key}[${i}]`
}));
}
return errors;
}
function validateNumber(options) {
const key = options.key;
const value = options.value;
const valueSpec = options.valueSpec;
let type = getType(value);
if (type === 'number' && value !== value) {
type = 'NaN';
}
if (type !== 'number') {
return [new ValidationError(key, value, `number expected, ${type} found`)];
}
if ('minimum' in valueSpec && value < valueSpec.minimum) {
return [new ValidationError(key, value, `${value} is less than the minimum value ${valueSpec.minimum}`)];
}
if ('maximum' in valueSpec && value > valueSpec.maximum) {
return [new ValidationError(key, value, `${value} is greater than the maximum value ${valueSpec.maximum}`)];
}
return [];
}
function validateFunction(options) {
const functionValueSpec = options.valueSpec;
const functionType = unbundle(options.value.type);
let stopKeyType;
let stopDomainValues = {};
let previousStopDomainValue;
let previousStopDomainZoom;
const isZoomFunction = functionType !== 'categorical' && options.value.property === undefined;
const isPropertyFunction = !isZoomFunction;
const isZoomAndPropertyFunction = getType(options.value.stops) === 'array' &&
getType(options.value.stops[0]) === 'array' &&
getType(options.value.stops[0][0]) === 'object';
const errors = validateObject({
key: options.key,
value: options.value,
valueSpec: options.styleSpec.function,
validateSpec: options.validateSpec,
style: options.style,
styleSpec: options.styleSpec,
objectElementValidators: {
stops: validateFunctionStops,
default: validateFunctionDefault
}
});
if (functionType === 'identity' && isZoomFunction) {
errors.push(new ValidationError(options.key, options.value, 'missing required property "property"'));
}
if (functionType !== 'identity' && !options.value.stops) {
errors.push(new ValidationError(options.key, options.value, 'missing required property "stops"'));
}
if (functionType === 'exponential' && options.valueSpec.expression && !supportsInterpolation(options.valueSpec)) {
errors.push(new ValidationError(options.key, options.value, 'exponential functions not supported'));
}
if (options.styleSpec.$version >= 8) {
if (isPropertyFunction && !supportsPropertyExpression(options.valueSpec)) {
errors.push(new ValidationError(options.key, options.value, 'property functions not supported'));
}
else if (isZoomFunction && !supportsZoomExpression(options.valueSpec)) {
errors.push(new ValidationError(options.key, options.value, 'zoom functions not supported'));
}
}
if ((functionType === 'categorical' || isZoomAndPropertyFunction) && options.value.property === undefined) {
errors.push(new ValidationError(options.key, options.value, '"property" property is required'));
}
return errors;
function validateFunctionStops(options) {
if (functionType === 'identity') {
return [new ValidationError(options.key, options.value, 'identity function may not have a "stops" property')];
}
let errors = [];
const value = options.value;
errors = errors.concat(validateArray({
key: options.key,
value,
valueSpec: options.valueSpec,
validateSpec: options.validateSpec,
style: options.style,
styleSpec: options.styleSpec,
arrayElementValidator: validateFunctionStop
}));
if (getType(value) === 'array' && value.length === 0) {
errors.push(new ValidationError(options.key, value, 'array must have at least one stop'));
}
return errors;
}
function validateFunctionStop(options) {
let errors = [];
const value = options.value;
const key = options.key;
if (getType(value) !== 'array') {
return [new ValidationError(key, value, `array expected, ${getType(value)} found`)];
}
if (value.length !== 2) {
return [new ValidationError(key, value, `array length 2 expected, length ${value.length} found`)];
}
if (isZoomAndPropertyFunction) {
if (getType(value[0]) !== 'object') {
return [new ValidationError(key, value, `object expected, ${getType(value[0])} found`)];
}
if (value[0].zoom === undefined) {
return [new ValidationError(key, value, 'object stop key must have zoom')];
}
if (value[0].value === undefined) {
return [new ValidationError(key, value, 'object stop key must have value')];
}
if (previousStopDomainZoom && previousStopDomainZoom > unbundle(value[0].zoom)) {
return [new ValidationError(key, value[0].zoom, 'stop zoom values must appear in ascending order')];
}
if (unbundle(value[0].zoom) !== previousStopDomainZoom) {
previousStopDomainZoom = unbundle(value[0].zoom);
previousStopDomainValue = undefined;
stopDomainValues = {};
}
errors = errors.concat(validateObject({
key: `${key}[0]`,
value: value[0],
valueSpec: { zoom: {} },
validateSpec: options.validateSpec,
style: options.style,
styleSpec: options.styleSpec,
objectElementValidators: { zoom: validateNumber, value: validateStopDomainValue }
}));
}
else {
errors = errors.concat(validateStopDomainValue({
key: `${key}[0]`,
value: value[0],
validateSpec: options.validateSpec,
style: options.style,
styleSpec: options.styleSpec
}, value));
}
if (isExpression(deepUnbundle(value[1]))) {
return errors.concat([new ValidationError(`${key}[1]`, value[1], 'expressions are not allowed in function stops.')]);
}
return errors.concat(options.validateSpec({
key: `${key}[1]`,
value: value[1],
valueSpec: functionValueSpec,
validateSpec: options.validateSpec,
style: options.style,
styleSpec: options.styleSpec
}));
}
function validateStopDomainValue(options, stop) {
const type = getType(options.value);
const value = unbundle(options.value);
const reportValue = options.value !== null ? options.value : stop;
if (!stopKeyType) {
stopKeyType = type;
}
else if (type !== stopKeyType) {
return [new ValidationError(options.key, reportValue, `${type} stop domain type must match previous stop domain type ${stopKeyType}`)];
}
if (type !== 'number' && type !== 'string' && type !== 'boolean') {
return [new ValidationError(options.key, reportValue, 'stop domain value must be a number, string, or boolean')];
}
if (type !== 'number' && functionType !== 'categorical') {
let message = `number expected, ${type} found`;
if (supportsPropertyExpression(functionValueSpec) && functionType === undefined) {
message += '\nIf you intended to use a categorical function, specify `"type": "categorical"`.';
}
return [new ValidationError(options.key, reportValue, message)];
}
if (functionType === 'categorical' && type === 'number' && (!isFinite(value) || Math.floor(value) !== value)) {
return [new ValidationError(options.key, reportValue, `integer expected, found ${value}`)];
}
if (functionType !== 'categorical' && type === 'number' && previousStopDomainValue !== undefined && value < previousStopDomainValue) {
return [new ValidationError(options.key, reportValue, 'stop domain values must appear in ascending order')];
}
else {
previousStopDomainValue = value;
}
if (functionType === 'categorical' && value in stopDomainValues) {
return [new ValidationError(options.key, reportValue, 'stop domain values must be unique')];
}
else {
stopDomainValues[value] = true;
}
return [];
}
function validateFunctionDefault(options) {
return options.validateSpec({
key: options.key,
value: options.value,
valueSpec: functionValueSpec,
validateSpec: options.validateSpec,
style: options.style,
styleSpec: options.styleSpec
});
}
}
function validateExpression(options) {
const expression = (options.expressionContext === 'property' ? createPropertyExpression : createExpression)(deepUnbundle(options.value), options.valueSpec);
if (expression.result === 'error') {
return expression.value.map((error) => {
return new ValidationError(`${options.key}${error.key}`, options.value, error.message);
});
}
const expressionObj = expression.value.expression || expression.value._styleExpression.expression;
if (options.expressionContext === 'property' && (options.propertyKey === 'text-font') &&
!expressionObj.outputDefined()) {
return [new ValidationError(options.key, options.value, `Invalid data expression for "${options.propertyKey}". Output values must be contained as literals within the expression.`)];
}
if (options.expressionContext === 'property' && options.propertyType === 'layout' &&
(!isStateConstant(expressionObj))) {
return [new ValidationError(options.key, options.value, '"feature-state" data expressions are not supported with layout properties.')];
}
if (options.expressionContext === 'filter' && !isStateConstant(expressionObj)) {
return [new ValidationError(options.key, options.value, '"feature-state" data expressions are not supported with filters.')];
}
if (options.expressionContext && options.expressionContext.indexOf('cluster') === 0) {
if (!isGlobalPropertyConstant(expressionObj, ['zoom', 'feature-state'])) {
return [new ValidationError(options.key, options.value, '"zoom" and "feature-state" expressions are not supported with cluster properties.')];
}
if (options.expressionContext === 'cluster-initial' && !isFeatureConstant(expressionObj)) {
return [new ValidationError(options.key, options.value, 'Feature data expressions are not supported with initial expression part of cluster properties.')];
}
}
return [];
}
function validateBoolean(options) {
const value = options.value;
const key = options.key;
const type = getType(value);
if (type !== 'boolean') {
return [new ValidationError(key, value, `boolean expected, ${type} found`)];
}
return [];
}
function validateColor(options) {
const key = options.key;
const value = options.value;
const type = getType(value);
if (type !== 'string') {
return [new ValidationError(key, value, `color expected, ${type} found`)];
}
if (!Color.parse(String(value))) { // cast String object to string primitive
return [new ValidationError(key, value, `color expected, "${value}" found`)];
}
return [];
}
function validateEnum(options) {
const key = options.key;
const value = options.value;
const valueSpec = options.valueSpec;
const errors = [];
if (Array.isArray(valueSpec.values)) { // <=v7
if (valueSpec.values.indexOf(unbundle(value)) === -1) {
errors.push(new ValidationError(key, value, `expected one of [${valueSpec.values.join(', ')}], ${JSON.stringify(value)} found`));
}
}
else { // >=v8
if (Object.keys(valueSpec.values).indexOf(unbundle(value)) === -1) {
errors.push(new ValidationError(key, value, `expected one of [${Object.keys(valueSpec.values).join(', ')}], ${JSON.stringify(value)} found`));
}
}
return errors;
}
function validateFilter(options) {
if (isExpressionFilter(deepUnbundle(options.value))) {
return validateExpression(extendBy({}, options, {
expressionContext: 'filter',
valueSpec: { value: 'boolean' }
}));
}
else {
return validateNonExpressionFilter(options);
}
}
function validateNonExpressionFilter(options) {
const value = options.value;
const key = options.key;
if (getType(value) !== 'array') {
return [new ValidationError(key, value, `array expected, ${getType(value)} found`)];
}
const styleSpec = options.styleSpec;
let type;
let errors = [];
if (value.length < 1) {
return [new ValidationError(key, value, 'filter array must have at least 1 element')];
}
errors = errors.concat(validateEnum({
key: `${key}[0]`,
value: value[0],
valueSpec: styleSpec.filter_operator,
style: options.style,
styleSpec: options.styleSpec
}));
switch (unbundle(value[0])) {
case '<':
case '<=':
case '>':
case '>=':
if (value.length >= 2 && unbundle(value[1]) === '$type') {
errors.push(new ValidationError(key, value, `"$type" cannot be use with operator "${value[0]}"`));
}
/* falls through */
case '==':
case '!=':
if (value.length !== 3) {
errors.push(new ValidationError(key, value, `filter array for operator "${value[0]}" must have 3 elements`));
}
/* falls through */
case 'in':
case '!in':
if (value.length >= 2) {
type = getType(value[1]);
if (type !== 'string') {
errors.push(new ValidationError(`${key}[1]`, value[1], `string expected, ${type} found`));
}
}
for (let i = 2; i < value.length; i++) {
type = getType(value[i]);
if (unbundle(value[1]) === '$type') {
errors = errors.concat(validateEnum({
key: `${key}[${i}]`,
value: value[i],
valueSpec: styleSpec.geometry_type,
style: options.style,
styleSpec: options.styleSpec
}));
}
else if (type !== 'string' && type !== 'number' && type !== 'boolean') {
errors.push(new ValidationError(`${key}[${i}]`, value[i], `string, number, or boolean expected, ${type} found`));
}
}
break;
case 'any':
case 'all':
case 'none':
for (let i = 1; i < value.length; i++) {
errors = errors.concat(validateNonExpressionFilter({
key: `${key}[${i}]`,
value: value[i],
style: options.style,
styleSpec: options.styleSpec
}));
}
break;
case 'has':
case '!has':
type = getType(value[1]);
if (value.length !== 2) {
errors.push(new ValidationError(key, value, `filter array for "${value[0]}" operator must have 2 elements`));
}
else if (type !== 'string') {
errors.push(new ValidationError(`${key}[1]`, value[1], `string expected, ${type} found`));
}
break;
}
return errors;
}
function validateProperty(options, propertyType) {
const key = options.key;
const validateSpec = options.validateSpec;
const style = options.style;
const styleSpec = options.styleSpec;
const value = options.value;
const propertyKey = options.objectKey;
const layerSpec = styleSpec[`${propertyType}_${options.layerType}`];
if (!layerSpec)
return [];
const transitionMatch = propertyKey.match(/^(.*)-transition$/);
if (propertyType === 'paint' && transitionMatch && layerSpec[transitionMatch[1]] && layerSpec[transitionMatch[1]].transition) {
return validateSpec({
key,
value,
valueSpec: styleSpec.transition,
style,
styleSpec
});
}
const valueSpec = options.valueSpec || layerSpec[propertyKey];
if (!valueSpec) {
return [new ValidationError(key, value, `unknown property "${propertyKey}"`)];
}
let tokenMatch;
if (getType(value) === 'string' && supportsPropertyExpression(valueSpec) && !valueSpec.tokens && (tokenMatch = /^{([^}]+)}$/.exec(value))) {
return [new ValidationError(key, value, `"${propertyKey}" does not support interpolation syntax\n` +
`Use an identity property function instead: \`{ "type": "identity", "property": ${JSON.stringify(tokenMatch[1])} }\`.`)];
}
const errors = [];
if (options.layerType === 'symbol') {
if (propertyKey === 'text-field' && style && !style.glyphs) {
errors.push(new ValidationError(key, value, 'use of "text-field" requires a style "glyphs" property'));
}
if (propertyKey === 'text-font' && isFunction$1(deepUnbundle(value)) && unbundle(value.type) === 'identity') {
errors.push(new ValidationError(key, value, '"text-font" does not support identity functions'));
}
}
return errors.concat(validateSpec({
key: options.key,
value,
valueSpec,
style,
styleSpec,
expressionContext: 'property',
propertyType,
propertyKey
}));
}
function validatePaintProperty(options) {
return validateProperty(options, 'paint');
}
function validateLayoutProperty(options) {
return validateProperty(options, 'layout');
}
function validateLayer(options) {
let errors = [];
const layer = options.value;
const key = options.key;
const style = options.style;
const styleSpec = options.styleSpec;
if (getType(layer) !== 'object') {
return [new ValidationError(key, layer, `object expected, ${getType(layer)} found`)];
}
if (!layer.type && !layer.ref) {
errors.push(new ValidationError(key, layer, 'either "type" or "ref" is required'));
}
let type = unbundle(layer.type);
const ref = unbundle(layer.ref);
if (layer.id) {
const layerId = unbundle(layer.id);
for (let i = 0; i < options.arrayIndex; i++) {
const otherLayer = style.layers[i];
if (unbundle(otherLayer.id) === layerId) {
errors.push(new ValidationError(key, layer.id, `duplicate layer id "${layer.id}", previously used at line ${otherLayer.id.__line__}`));
}
}
}
if ('ref' in layer) {
['type', 'source', 'source-layer', 'filter', 'layout'].forEach((p) => {
if (p in layer) {
errors.push(new ValidationError(key, layer[p], `"${p}" is prohibited for ref layers`));
}
});
let parent;
style.layers.forEach((layer) => {
if (unbundle(layer.id) === ref)
parent = layer;
});
if (!parent) {
errors.push(new ValidationError(key, layer.ref, `ref layer "${ref}" not found`));
}
else if (parent.ref) {
errors.push(new ValidationError(key, layer.ref, 'ref cannot reference another ref layer'));
}
else {
type = unbundle(parent.type);
}
}
else if (type !== 'background') {
if (!layer.source) {
errors.push(new ValidationError(key, layer, 'missing required property "source"'));
}
else {
const source = style.sources && style.sources[layer.source];
const sourceType = source && unbundle(source.type);
if (!source) {
errors.push(new ValidationError(key, layer.source, `source "${layer.source}" not found`));
}
else if (sourceType === 'vector' && type === 'raster') {
errors.push(new ValidationError(key, layer.source, `layer "${layer.id}" requires a raster source`));
}
else if (sourceType !== 'raster-dem' && type === 'hillshade') {
errors.push(new ValidationError(key, layer.source, `layer "${layer.id}" requires a raster-dem source`));
}
else if (sourceType !== 'raster-dem' && type === 'color-relief') {
errors.push(new ValidationError(key, layer.source, `layer "${layer.id}" requires a raster-dem source`));
}
else if (sourceType === 'raster' && type !== 'raster') {
errors.push(new ValidationError(key, layer.source, `layer "${layer.id}" requires a vector source`));
}
else if (sourceType === 'vector' && !layer['source-layer']) {
errors.push(new ValidationError(key, layer, `layer "${layer.id}" must specify a "source-layer"`));
}
else if (sourceType === 'raster-dem' && (type !== 'hillshade' && type !== 'color-relief')) {
errors.push(new ValidationError(key, layer.source, 'raster-dem source can only be used with layer type \'hillshade\' or \'color-relief\'.'));
}
else if (type === 'line' && layer.paint && layer.paint['line-gradient'] &&
(sourceType !== 'geojson' || !source.lineMetrics)) {
errors.push(new ValidationError(key, layer, `layer "${layer.id}" specifies a line-gradient, which requires a GeoJSON source with \`lineMetrics\` enabled.`));
}
}
}
errors = errors.concat(validateObject({
key,
value: layer,
valueSpec: styleSpec.layer,
style: options.style,
styleSpec: options.styleSpec,
validateSpec: options.validateSpec,
objectElementValidators: {
'*'() {
return [];
},
// We don't want to enforce the spec's `"requires": true` for backward compatibility with refs;
// the actual requirement is validated above. See https://github.com/mapbox/mapbox-gl-js/issues/5772.
type() {
return options.validateSpec({
key: `${key}.type`,
value: layer.type,
valueSpec: styleSpec.layer.type,
style: options.style,
styleSpec: options.styleSpec,
validateSpec: options.validateSpec,
object: layer,
objectKey: 'type'
});
},
filter: validateFilter,
layout(options) {
return validateObject({
layer,
key: options.key,
value: options.value,
style: options.style,
styleSpec: options.styleSpec,
validateSpec: options.validateSpec,
objectElementValidators: {
'*'(options) {
return validateLayoutProperty(extendBy({ layerType: type }, options));
}
}
});
},
paint(options) {
return validateObject({
layer,
key: options.key,
value: options.value,
style: options.style,
styleSpec: options.styleSpec,
validateSpec: options.validateSpec,
objectElementValidators: {
'*'(options) {
return validatePaintProperty(extendBy({ layerType: type }, options));
}
}
});
}
}
}));
return errors;
}
function validateString(options) {
const value = options.value;
const key = options.key;
const type = getType(value);
if (type !== 'string') {
return [new ValidationError(key, value, `string expected, ${type} found`)];
}
return [];
}
function validateRasterDEMSource(options) {
var _a;
const sourceName = (_a = options.sourceName) !== null && _a !== void 0 ? _a : '';
const rasterDEM = options.value;
const styleSpec = options.styleSpec;
const rasterDEMSpec = styleSpec.source_raster_dem;
const style = options.style;
let errors = [];
const rootType = getType(rasterDEM);
if (rasterDEM === undefined) {
return errors;
}
else if (rootType !== 'object') {
errors.push(new ValidationError('source_raster_dem', rasterDEM, `object expected, ${rootType} found`));
return errors;
}
const encoding = unbundle(rasterDEM.encoding);
const isCustomEncoding = encoding === 'custom';
const customEncodingKeys = ['redFactor', 'greenFactor', 'blueFactor', 'baseShift'];
const encodingName = options.value.encoding ? `"${options.value.encoding}"` : 'Default';
for (const key in rasterDEM) {
if (!isCustomEncoding && customEncodingKeys.includes(key)) {
errors.push(new ValidationError(key, rasterDEM[key], `In "${sourceName}": "${key}" is only valid when "encoding" is set to "custom". ${encodingName} encoding found`));
}
else if (rasterDEMSpec[key]) {
errors = errors.concat(options.validateSpec({
key,
value: rasterDEM[key],
valueSpec: rasterDEMSpec[key],
validateSpec: options.validateSpec,
style,
styleSpec
}));
}
else {
errors.push(new ValidationError(key, rasterDEM[key], `unknown property "${key}"`));
}
}
return errors;
}
const objectElementValidators = {
promoteId: validatePromoteId
};
function validateSource(options) {
const value = options.value;
const key = options.key;
const styleSpec = options.styleSpec;
const style = options.style;
const validateSpec = options.validateSpec;
if (!value.type) {
return [new ValidationError(key, value, '"type" is required')];
}
const type = unbundle(value.type);
let errors;
switch (type) {
case 'vector':
case 'raster':
errors = validateObject({
key,
value,
valueSpec: styleSpec[`source_${type.replace('-', '_')}`],
style: options.style,
styleSpec,
objectElementValidators,
validateSpec,
});
return errors;
case 'raster-dem':
errors = validateRasterDEMSource({
sourceName: key,
value,
style: options.style,
styleSpec,
validateSpec,
});
return errors;
case 'geojson':
errors = validateObject({
key,
value,
valueSpec: styleSpec.source_geojson,
style,
styleSpec,
validateSpec,
objectElementValidators
});
if (value.cluster) {
for (const prop in value.clusterProperties) {
const [operator, mapExpr] = value.clusterProperties[prop];
const reduceExpr = typeof operator === 'string' ? [operator, ['accumulated'], ['get', prop]] : operator;
errors.push(...validateExpression({
key: `${key}.${prop}.map`,
value: mapExpr,
expressionContext: 'cluster-map'
}));
errors.push(...validateExpression({
key: `${key}.${prop}.reduce`,
value: reduceExpr,
expressionContext: 'cluster-reduce'
}));
}
}
return errors;
case 'video':
return validateObject({
key,
value,
valueSpec: styleSpec.source_video,
style,
validateSpec,
styleSpec
});
case 'image':
return validateObject({
key,
value,
valueSpec: styleSpec.source_image,
style,
validateSpec,
styleSpec
});
case 'canvas':
return [new ValidationError(key, null, 'Please use runtime APIs to add canvas sources, rather than including them in stylesheets.', 'source.canvas')];
default:
return validateEnum({
key: `${key}.type`,
value: value.type,
valueSpec: { values: ['vector', 'raster', 'raster-dem', 'geojson', 'video', 'image'] }});
}
}
function validatePromoteId({ key, value }) {
if (getType(value) === 'string') {
return validateString({ key, value });
}
else {
const errors = [];
for (const prop in value) {
errors.push(...validateString({ key: `${key}.${prop}`, value: value[prop] }));
}
return errors;
}
}
function validateLight(options) {
const light = options.value;
const styleSpec = options.styleSpec;
const lightSpec = styleSpec.light;
const style = options.style;
let errors = [];
const rootType = getType(light);
if (light === undefined) {
return errors;
}
else if (rootType !== 'object') {
errors = errors.concat([new ValidationError('light', light, `object expected, ${rootType} found`)]);
return errors;
}
for (const key in light) {
const transitionMatch = key.match(/^(.*)-transition$/);
if (transitionMatch && lightSpec[transitionMatch[1]] && lightSpec[transitionMatch[1]].transition) {
errors = errors.concat(options.validateSpec({
key,
value: light[key],
valueSpec: styleSpec.transition,
validateSpec: options.validateSpec,
style,
styleSpec
}));
}
else if (lightSpec[key]) {
errors = errors.concat(options.validateSpec({
key,
value: light[key],
valueSpec: lightSpec[key],
validateSpec: options.validateSpec,
style,
styleSpec
}));
}
else {
errors = errors.concat([new ValidationError(key, light[key], `unknown property "${key}"`)]);
}
}
return errors;
}
function validateSky(options) {
const sky = options.value;
const styleSpec = options.styleSpec;
const skySpec = styleSpec.sky;
const style = options.style;
const rootType = getType(sky);
if (sky === undefined) {
return [];
}
else if (rootType !== 'object') {
return [new ValidationError('sky', sky, `object expected, ${rootType} found`)];
}
let errors = [];
for (const key in sky) {
if (skySpec[key]) {
errors = errors.concat(options.validateSpec({
key,
value: sky[key],
valueSpec: skySpec[key],
style,
styleSpec
}));
}
else {
errors = errors.concat([new ValidationError(key, sky[key], `unknown property "${key}"`)]);
}
}
return errors;
}
function validateTerrain(options) {
const terrain = options.value;
const styleSpec = options.styleSpec;
const terrainSpec = styleSpec.terrain;
const style = options.style;
let errors = [];
const rootType = getType(terrain);
if (terrain === undefined) {
return errors;
}
else if (rootType !== 'object') {
errors = errors.concat([new ValidationError('terrain', terrain, `object expected, ${rootType} found`)]);
return errors;
}
for (const key in terrain) {
if (terrainSpec[key]) {
errors = errors.concat(options.validateSpec({
key,
value: terrain[key],
valueSpec: terrainSpec[key],
validateSpec: options.validateSpec,
style,
styleSpec
}));
}
else {
errors = errors.concat([new ValidationError(key, terrain[key], `unknown property "${key}"`)]);
}
}
return errors;
}
function validateFormatted(options) {
if (validateString(options).length === 0) {
return [];
}
return validateExpression(options);
}
function validateImage(options) {
if (validateString(options).length === 0) {
return [];
}
return validateExpression(options);
}
function validatePadding(options) {
const key = options.key;
const value = options.value;
const type = getType(value);
if (type === 'array') {
if (value.length < 1 || value.length > 4) {
return [new ValidationError(key, value, `padding requires 1 to 4 values; ${value.length} values found`)];
}
const arrayElementSpec = {
type: 'number'
};
let errors = [];
for (let i = 0; i < value.length; i++) {
errors = errors.concat(options.validateSpec({
key: `${key}[${i}]`,
value: value[i],
validateSpec: options.validateSpec,
valueSpec: arrayElementSpec
}));
}
return errors;
}
else {
return validateNumber({
key,
value,
valueSpec: {}
});
}
}
function validateNumberArray(options) {
const key = options.key;
const value = options.value;
const type = getType(value);
if (type === 'array') {
const arrayElementSpec = {
type: 'number'
};
if (value.length < 1) {
return [new ValidationError(key, value, 'array length at least 1 expected, length 0 found')];
}
let errors = [];
for (let i = 0; i < value.length; i++) {
errors = errors.concat(options.validateSpec({
key: `${key}[${i}]`,
value: value[i],
validateSpec: options.validateSpec,
valueSpec: arrayElementSpec
}));
}
return errors;
}
else {
return validateNumber({
key,
value,
valueSpec: {}
});
}
}
function validateColorArray(options) {
const key = options.key;
const value = options.value;
const type = getType(value);
if (type === 'array') {
if (value.length < 1) {
return [new ValidationError(key, value, 'array length at least 1 expected, length 0 found')];
}
let errors = [];
for (let i = 0; i < value.length; i++) {
errors = errors.concat(validateColor({
key: `${key}[${i}]`,
value: value[i]}));
}
return errors;
}
else {
return validateColor({
key,
value});
}
}
function validateVariableAnchorOffsetCollection(options) {
const key = options.key;
const value = options.value;
const type = getType(value);
const styleSpec = options.styleSpec;
if (type !== 'array' || value.length < 1 || value.length % 2 !== 0) {
return [new ValidationError(key, value, 'variableAnchorOffsetCollection requires a non-empty array of even length')];
}
let errors = [];
for (let i = 0; i < value.length; i += 2) {
// Elements in even positions should be values from text-anchor enum
errors = errors.concat(validateEnum({
key: `${key}[${i}]`,
value: value[i],
valueSpec: styleSpec['layout_symbol']['text-anchor']
}));
// Elements in odd positions should be points (2-element numeric arrays)
errors = errors.concat(validateArray({
key: `${key}[${i + 1}]`,
value: value[i + 1],
valueSpec: {
length: 2,
value: 'number'
},
validateSpec: options.validateSpec,
style: options.style,
styleSpec
}));
}
return errors;
}
function validateSprite(options) {
let errors = [];
const sprite = options.value;
const key = options.key;
if (!Array.isArray(sprite)) {
return validateString({
key,
value: sprite
});
}
else {
const allSpriteIds = [];
const allSpriteURLs = [];
for (const i in sprite) {
if (sprite[i].id && allSpriteIds.includes(sprite[i].id))
errors.push(new ValidationError(key, sprite, `all the sprites' ids must be unique, but ${sprite[i].id} is duplicated`));
allSpriteIds.push(sprite[i].id);
if (sprite[i].url && allSpriteURLs.includes(sprite[i].url))
errors.push(new ValidationError(key, sprite, `all the sprites' URLs must be unique, but ${sprite[i].url} is duplicated`));
allSpriteURLs.push(sprite[i].url);
const pairSpec = {
id: {
type: 'string',
required: true,
},
url: {
type: 'string',
required: true,
}
};
errors = errors.concat(validateObject({
key: `${key}[${i}]`,
value: sprite[i],
valueSpec: pairSpec,
validateSpec: options.validateSpec,
}));
}
return errors;
}
}
function validateProjection(options) {
const projection = options.value;
const styleSpec = options.styleSpec;
const projectionSpec = styleSpec.projection;
const style = options.style;
const rootType = getType(projection);
if (projection === undefined) {
return [];
}
else if (rootType !== 'object') {
return [new ValidationError('projection', projection, `object expected, ${rootType} found`)];
}
let errors = [];
for (const key in projection) {
if (projectionSpec[key]) {
errors = errors.concat(options.validateSpec({
key,
value: projection[key],
valueSpec: projectionSpec[key],
style,
styleSpec
}));
}
else {
errors = errors.concat([new ValidationError(key, projection[key], `unknown property "${key}"`)]);
}
}
return errors;
}
function validateProjectionDefinition(options) {
const key = options.key;
let value = options.value;
value = value instanceof String ? value.valueOf() : value;
const type = getType(value);
if (type === 'array' && !isProjectionDefinitionValue(value) && !isPropertyValueSpecification(value)) {
return [new ValidationError(key, value, `projection expected, invalid array ${JSON.stringify(value)} found`)];
}
else if (!['array', 'string'].includes(type)) {
return [new ValidationError(key, value, `projection expected, invalid type "${type}" found`)];
}
return [];
}
function isPropertyValueSpecification(value) {
if (['interpolate', 'step', 'literal'].includes(value[0])) {
return true;
}
return false;
}
function isProjectionDefinitionValue(value) {
return Array.isArray(value) &&
value.length === 3 &&
typeof value[0] === 'string' &&
typeof value[1] === 'string' &&
typeof value[2] === 'number';
}
function isObjectLiteral(anything) {
return Boolean(anything) && anything.constructor === Object;
}
function validateState(options) {
if (!isObjectLiteral(options.value)) {
return [
new ValidationError(options.key, options.value, `object expected, ${getType(options.value)} found`),
];
}
return [];
}
const VALIDATORS = {
'*'() {
return [];
},
'array': validateArray,
'boolean': validateBoolean,
'number': validateNumber,
'color': validateColor,
'constants': validateConstants,
'enum': validateEnum,
'filter': validateFilter,
'function': validateFunction,
'layer': validateLayer,
'object': validateObject,
'source': validateSource,
'light': validateLight,
'sky': validateSky,
'terrain': validateTerrain,
'projection': validateProjection,
'projectionDefinition': validateProjectionDefinition,
'string': validateString,
'formatted': validateFormatted,
'resolvedImage': validateImage,
'padding': validatePadding,
'numberArray': validateNumberArray,
'colorArray': validateColorArray,
'variableAnchorOffsetCollection': validateVariableAnchorOffsetCollection,
'sprite': validateSprite,
'state': validateState
};
/**
* Main recursive validation function used internally.
* You should use `validateStyleMin` in the browser or `validateStyle` in node env.
* @param options - the options object
* @param options.key - string representing location of validation in style tree. Used only
* for more informative error reporting.
* @param options.value - current value from style being evaluated. May be anything from a
* high level object that needs to be descended into deeper or a simple
* scalar value.
* @param options.valueSpec - current spec being evaluated. Tracks value.
* @param options.styleSpec - current full spec being evaluated.
* @param options.validateSpec - the validate function itself
* @param options.style - the style object
* @param options.objectElementValidators - optional object of functions that will be called
* @returns an array of errors, or an empty array if no errors are found.
*/
function validate(options) {
const value = options.value;
const valueSpec = options.valueSpec;
const styleSpec = options.styleSpec;
options.validateSpec = validate;
if (valueSpec.expression && isFunction$1(unbundle(value))) {
return validateFunction(options);
}
else if (valueSpec.expression && isExpression(deepUnbundle(value))) {
return validateExpression(options);
}
else if (valueSpec.type && VALIDATORS[valueSpec.type]) {
return VALIDATORS[valueSpec.type](options);
}
else {
const valid = validateObject(extendBy({}, options, {
valueSpec: valueSpec.type ? styleSpec[valueSpec.type] : valueSpec
}));
return valid;
}
}
function validateGlyphsUrl(options) {
const value = options.value;
const key = options.key;
const errors = validateString(options);
if (errors.length)
return errors;
if (value.indexOf('{fontstack}') === -1) {
errors.push(new ValidationError(key, value, '"glyphs" url must include a "{fontstack}" token'));
}
if (value.indexOf('{range}') === -1) {
errors.push(new ValidationError(key, value, '"glyphs" url must include a "{range}" token'));
}
return errors;
}
/**
* Validate a MapLibre style against the style specification.
* Use this when running in the browser.
*
* @param style - The style to be validated.
* @param styleSpec - The style specification to validate against.
* If omitted, the latest style spec is used.
* @returns an array of errors, or an empty array if no errors are found.
* @example
* const validate = require('@maplibre/maplibre-gl-style-spec/').validateStyleMin;
* const errors = validate(style);
*/
function validateStyleMin(style, styleSpec = v8Spec) {
let errors = [];
errors = errors.concat(validate({
key: '',
value: style,
valueSpec: styleSpec.$root,
styleSpec,
style,
validateSpec: validate,
objectElementValidators: {
glyphs: validateGlyphsUrl,
'*'() {
return [];
}
}
}));
if (style['constants']) {
errors = errors.concat(validateConstants({
key: 'constants',
value: style['constants']}));
}
return sortErrors(errors);
}
validateStyleMin.source = wrapCleanErrors(injectValidateSpec(validateSource));
validateStyleMin.sprite = wrapCleanErrors(injectValidateSpec(validateSprite));
validateStyleMin.glyphs = wrapCleanErrors(injectValidateSpec(validateGlyphsUrl));
validateStyleMin.light = wrapCleanErrors(injectValidateSpec(validateLight));
validateStyleMin.sky = wrapCleanErrors(injectValidateSpec(validateSky));
validateStyleMin.terrain = wrapCleanErrors(injectValidateSpec(validateTerrain));
validateStyleMin.state = wrapCleanErrors(injectValidateSpec(validateState));
validateStyleMin.layer = wrapCleanErrors(injectValidateSpec(validateLayer));
validateStyleMin.filter = wrapCleanErrors(injectValidateSpec(validateFilter));
validateStyleMin.paintProperty = wrapCleanErrors(injectValidateSpec(validatePaintProperty));
validateStyleMin.layoutProperty = wrapCleanErrors(injectValidateSpec(validateLayoutProperty));
function injectValidateSpec(validator) {
return function (options) {
return validator(Object.assign({}, options, { validateSpec: validate }));
};
}
function sortErrors(errors) {
return [].concat(errors).sort((a, b) => {
return a.line - b.line;
});
}
function wrapCleanErrors(inner) {
return function (...args) {
return sortErrors(inner.apply(this, args));
};
}
// Note: This regex matches even invalid JSON strings, but since were
// working on the output of `JSON.stringify` we know that only valid strings
// are present (unless the user supplied a weird `options.indent` but in
// that case we dont care since the output would be invalid anyway).
const stringOrChar = /("(?:[^\\"]|\\.)*")|[:,]/g;
function stringify(passedObj, options = {}) {
const indent = JSON.stringify(
[1],
undefined,
options.indent === undefined ? 2 : options.indent
).slice(2, -3);
const maxLength =
indent === ""
? Infinity
: options.maxLength === undefined
? 80
: options.maxLength;
let { replacer } = options;
return (function _stringify(obj, currentIndent, reserved) {
if (obj && typeof obj.toJSON === "function") {
obj = obj.toJSON();
}
const string = JSON.stringify(obj, replacer);
if (string === undefined) {
return string;
}
const length = maxLength - currentIndent.length - reserved;
if (string.length <= length) {
const prettified = string.replace(
stringOrChar,
(match, stringLiteral) => {
return stringLiteral || `${match} `;
}
);
if (prettified.length <= length) {
return prettified;
}
}
if (replacer != null) {
obj = JSON.parse(string);
replacer = undefined;
}
if (typeof obj === "object" && obj !== null) {
const nextIndent = currentIndent + indent;
const items = [];
let index = 0;
let start;
let end;
if (Array.isArray(obj)) {
start = "[";
end = "]";
const { length } = obj;
for (; index < length; index++) {
items.push(
_stringify(obj[index], nextIndent, index === length - 1 ? 0 : 1) ||
"null"
);
}
} else {
start = "{";
end = "}";
const keys = Object.keys(obj);
const { length } = keys;
for (; index < length; index++) {
const key = keys[index];
const keyPart = `${JSON.stringify(key)}: `;
const value = _stringify(
obj[key],
nextIndent,
keyPart.length + (index === length - 1 ? 0 : 1)
);
if (value !== undefined) {
items.push(keyPart + value);
}
}
}
if (items.length > 0) {
return [start, indent + items.join(`,\n${nextIndent}`), end].join(
`\n${currentIndent}`
);
}
}
return string;
})(passedObj, "", 0);
}
function sortKeysBy(obj, reference) {
const result = {};
for (const key in reference) {
if (obj[key] !== undefined) {
result[key] = obj[key];
}
}
for (const key in obj) {
if (result[key] === undefined) {
result[key] = obj[key];
}
}
return result;
}
/**
* Format a MapLibre Style. Returns a stringified style with its keys
* sorted in the same order as the reference style.
*
* The optional `space` argument is passed to
* [`JSON.stringify`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify)
* to generate formatted output.
*
* If `space` is unspecified, a default of `2` spaces will be used.
*
* @private
* @param {Object} style a MapLibre Style
* @param {number} [space] space argument to pass to `JSON.stringify`
* @returns {string} stringified formatted JSON
* @example
* var fs = require('fs');
* var format = require('maplibre-gl-style-spec').format;
* var style = fs.readFileSync('./source.json', 'utf8');
* fs.writeFileSync('./dest.json', format(style));
* fs.writeFileSync('./dest.min.json', format(style, 0));
*/
function format(style, space = 2) {
style = sortKeysBy(style, v8Spec.$root);
if (style.layers) {
style.layers = style.layers.map((layer) => sortKeysBy(layer, v8Spec.layer));
}
return stringify(style, { indent: space });
}
function eachLayout(layer, callback) {
for (const k in layer) {
if (k.indexOf('layout') === 0) {
callback(layer[k], k);
}
}
}
function eachPaint(layer, callback) {
for (const k in layer) {
if (k.indexOf('paint') === 0) {
callback(layer[k], k);
}
}
}
function resolveConstant(style, value) {
if (typeof value === 'string' && value[0] === '@') {
return resolveConstant(style, style.constants[value]);
}
else {
return value;
}
}
function isFunction(value) {
return Array.isArray(value.stops);
}
function renameProperty(obj, from, to) {
obj[to] = obj[from];
delete obj[from];
}
function migrateV8(style) {
style.version = 8;
// Rename properties, reverse coordinates in source and layers
eachSource(style, (source) => {
if (source.type === 'video' && source['url'] !== undefined) {
renameProperty(source, 'url', 'urls');
}
if (source.type === 'video') {
source.coordinates.forEach((coord) => {
return coord.reverse();
});
}
});
eachLayer(style, (layer) => {
eachLayout(layer, (layout) => {
if (layout['symbol-min-distance'] !== undefined) {
renameProperty(layout, 'symbol-min-distance', 'symbol-spacing');
}
});
eachPaint(layer, (paint) => {
if (paint['background-image'] !== undefined) {
renameProperty(paint, 'background-image', 'background-pattern');
}
if (paint['line-image'] !== undefined) {
renameProperty(paint, 'line-image', 'line-pattern');
}
if (paint['fill-image'] !== undefined) {
renameProperty(paint, 'fill-image', 'fill-pattern');
}
});
});
// Inline Constants
eachProperty(style, { paint: true, layout: true }, (property) => {
const value = resolveConstant(style, property.value);
if (isFunction(value)) {
value.stops.forEach((stop) => {
stop[1] = resolveConstant(style, stop[1]);
});
}
property.set(value);
});
delete style['constants'];
eachLayer(style, (layer) => {
// get rid of text-max-size, icon-max-size
// turn text-size, icon-size into layout properties
// https://github.com/mapbox/mapbox-gl-style-spec/issues/255
eachLayout(layer, (layout) => {
delete layout['text-max-size'];
delete layout['icon-max-size'];
});
eachPaint(layer, (paint) => {
if (paint['text-size']) {
if (!layer.layout)
layer.layout = {};
layer.layout['text-size'] = paint['text-size'];
delete paint['text-size'];
}
if (paint['icon-size']) {
if (!layer.layout)
layer.layout = {};
layer.layout['icon-size'] = paint['icon-size'];
delete paint['icon-size'];
}
});
});
function migrateFontStack(font) {
function splitAndTrim(string) {
return string.split(',').map((s) => {
return s.trim();
});
}
if (Array.isArray(font)) {
// Assume it's a previously migrated font-array.
return font;
}
else if (typeof font === 'string') {
return splitAndTrim(font);
}
else if (typeof font === 'object') {
font.stops.forEach((stop) => {
stop[1] = splitAndTrim(stop[1]);
});
return font;
}
else {
throw new Error('unexpected font value');
}
}
eachLayer(style, (layer) => {
eachLayout(layer, (layout) => {
if (layout['text-font']) {
layout['text-font'] = migrateFontStack(layout['text-font']);
}
});
});
// Reverse order of symbol layers. This is an imperfect migration.
//
// The order of a symbol layer in the layers list affects two things:
// - how it is drawn relative to other layers (like oneway arrows below bridges)
// - the placement priority compared to other layers
//
// It's impossible to reverse the placement priority without breaking the draw order
// in some cases. This migration only reverses the order of symbol layers that
// are above all other types of layers.
//
// Symbol layers that are at the top of the map preserve their priority.
// Symbol layers that are below another type (line, fill) of layer preserve their draw order.
let firstSymbolLayer = 0;
for (let i = style.layers.length - 1; i >= 0; i--) {
const layer = style.layers[i];
if (layer.type !== 'symbol') {
firstSymbolLayer = i + 1;
break;
}
}
const symbolLayers = style.layers.splice(firstSymbolLayer);
symbolLayers.reverse();
style.layers = style.layers.concat(symbolLayers);
return style;
}
/**
* Migrate the given style object in place to use expressions. Specifically,
* this will convert (a) "stop" functions, and (b) legacy filters to their
* expression equivalents.
* @param style The style object to migrate.
* @returns The migrated style object.
*/
function expressions(style) {
const converted = [];
eachLayer(style, (layer) => {
if (layer.filter) {
layer.filter = convertFilter(layer.filter);
}
});
eachProperty(style, { paint: true, layout: true }, ({ path, key, value, reference, set }) => {
if (isExpression(value) || key.endsWith('-transition') || reference === null)
return;
if (typeof value === 'object' && !Array.isArray(value)) {
set(convertFunction(value, reference));
converted.push(path.join('.'));
}
else if (reference.tokens && typeof value === 'string') {
set(convertTokenString(value));
}
});
return style;
}
/**
* Migrate color style values to supported format.
*
* @param colorToMigrate Color value to migrate, could be a string or an expression.
* @returns Color style value in supported format.
*/
function migrateColors(colorToMigrate) {
return JSON.parse(migrateHslColors(JSON.stringify(colorToMigrate)));
}
/**
* Created to migrate from colors supported by the former CSS color parsing
* library `csscolorparser` but not compliant with the CSS Color specification,
* like `hsl(900, 0.15, 90%)`.
*
* @param colorToMigrate Serialized color style value.
* @returns A serialized color style value in which all non-standard hsl color values
* have been converted to a format that complies with the CSS Color specification.
*
* @example
* migrateHslColors('"hsl(900, 0.15, 90%)"'); // returns '"hsl(900, 15%, 90%)"'
* migrateHslColors('"hsla(900, .15, .9)"'); // returns '"hsl(900, 15%, 90%)"'
* migrateHslColors('"hsl(900, 15%, 90%)"'); // returns '"hsl(900, 15%, 90%)"' - no changes
*/
function migrateHslColors(colorToMigrate) {
return colorToMigrate.replace(/"hsla?\((.+?)\)"/gi, (match, hslArgs) => {
const argsMatch = hslArgs.match(/^(.+?)\s*,\s*(.+?)\s*,\s*(.+?)(?:\s*,\s*(.+))?$/i);
if (argsMatch) {
let [h, s, l, a] = argsMatch.slice(1);
[s, l] = [s, l].map(v => v.endsWith('%') ? v : `${parseFloat(v) * 100}%`);
return `"hsl${typeof a === 'string' ? 'a' : ''}(${[h, s, l, a].filter(Boolean).join(',')})"`;
}
return match;
});
}
/**
* Migrate a Mapbox/MapLibre GL Style to the latest version.
*
* @param style - a MapLibre Style
* @returns a migrated style
* @example
* const fs = require('fs');
* const migrate = require('@maplibre/maplibre-gl-style-spec').migrate;
* const style = fs.readFileSync('./style.json', 'utf8');
* fs.writeFileSync('./style.json', JSON.stringify(migrate(style)));
*/
function migrate(style) {
let migrated = false;
if (style.version === 7) {
style = migrateV8(style);
migrated = true;
}
if (style.version === 8) {
migrated = !!expressions(style);
migrated = true;
}
eachProperty(style, { paint: true, layout: true }, ({ value, reference, set }) => {
if ((reference === null || reference === void 0 ? void 0 : reference.type) === 'color') {
set(migrateColors(value));
}
});
if (!migrated) {
throw new Error(`Cannot migrate from ${style.version}`);
}
return style;
}
const v8 = v8Spec;
const expression = {
StyleExpression,
StylePropertyFunction,
ZoomConstantExpression,
ZoomDependentExpression,
createExpression,
createPropertyExpression,
isExpression,
isExpressionFilter,
isZoomExpression,
normalizePropertyExpression,
};
const styleFunction = {
convertFunction,
createFunction,
isFunction: isFunction$1
};
const visit = { eachLayer, eachProperty, eachSource };
export { Color, ColorArray, ColorType, CompoundExpression, EvaluationContext, FormatExpression, Formatted, FormattedSection, FormattedType, Interpolate, Literal, NullType, NumberArray, Padding, ParsingError, ProjectionDefinition, ProjectionDefinitionType, ResolvedImage, Step, StyleExpression, StylePropertyFunction, ValidationError, VariableAnchorOffsetCollection, ZoomConstantExpression, ZoomDependentExpression, classifyRings, convertFilter, convertFunction, createExpression, createFunction, createPropertyExpression, derefLayers, diff, emptyStyle, expression, expressions$1 as expressions, featureFilter, format, styleFunction as function, groupByLayout, interpolateFactory as interpolates, isExpression, isFunction$1 as isFunction, isZoomExpression, v8Spec as latest, migrate, normalizePropertyExpression, supportsPropertyExpression, typeToString as toString, typeOf, v8, validate, validateStyleMin, visit };
//# sourceMappingURL=index.mjs.map