planning
All checks were successful
Publish To Prod / deploy_and_publish (push) Successful in 35s

This commit is contained in:
2024-10-14 09:15:30 +02:00
parent bcba00a730
commit 6e64e138e2
21059 changed files with 2317811 additions and 1 deletions

10
node_modules/@mapbox/mapbox-gl-style-spec/.eslintrc generated vendored Normal file
View File

@@ -0,0 +1,10 @@
{
"overrides": [
{
"files": ["rollup.config.js", "test.js"],
"rules": {
"flowtype/require-valid-file-annotation": "off"
}
}
]
}

564
node_modules/@mapbox/mapbox-gl-style-spec/CHANGELOG.md generated vendored Normal file
View File

@@ -0,0 +1,564 @@
## 13.27.0
### Bug fixes 🐞
* Fix overwriting all feature ids while setting promoteIds on other layers with an object. ([#12322](https://github.com/mapbox/mapbox-gl-js/pull/12322)) (h/t [yongjun21](https://github.com/yongjun21))
## 13.26.0
### Features ✨
* Add unit option to number-format expression. ([#11839](https://github.com/mapbox/mapbox-gl-js/pull/11839)) (h/t [varna](https://github.com/varna))
### Bug fixes 🐞
* Fix a bug where `id` expression didn't correctly handle a value of 0. ([#12000](https://github.com/mapbox/mapbox-gl-js/pull/12000))
## 13.25.0
### Features ✨
* Extend atmospheric `fog` with three new style specification properties: `high-color`, `space-color` and `star-intensity` to allow the design of atmosphere around the globe and night skies. ([#11590](https://github.com/mapbox/mapbox-gl-js/pull/11590))
* Add a new line layer paint property in the style specification: `line-trim-offset` that can be used to create a custom fade out with improved update performance over `line-gradient`. ([#11570](https://github.com/mapbox/mapbox-gl-js/pull/11570))
### 🐞 Bug fixes
* Add `source` field requirement to terrain exaggeration in the style specification. ([#11664](https://github.com/mapbox/mapbox-gl-js/pull/11664))
## 13.24.0
### 🐞 Bug fixes
* Fix error on `gl-style-validate` script. ([#11538](https://github.com/mapbox/mapbox-gl-js/pull/11538))
* Allow the second argument to the `in` expression operator to be an empty string. ([#11547](https://github.com/mapbox/mapbox-gl-js/pull/11547))
* Fix error on some valid `filter` expressions. ([#11475](https://github.com/mapbox/mapbox-gl-js/pull/11475))
## 13.23.1
### ✨ Features and improvements
* Improve `coalesce` expressions to return a `ResolvedImage` when images are missing. ([#11371](https://github.com/mapbox/mapbox-gl-js/pull/11371))
## 13.23.0
### ✨ Features and improvements
* Add a `projection` root property that allows a non-mercator projection to be set as a style's default projection. ([#11124](https://github.com/mapbox/mapbox-gl-js/pull/11124))
* Add support for using `["pitch"]` and `["distance-from-camera"]` expressions within the `filter` of a symbol layer. ([#10795](https://github.com/mapbox/mapbox-gl-js/pull/10795))
## 13.22.0
### ✨ Features and improvements
* Added `protected` field to mapbox-api-supported validation. ([#10968](https://github.com/mapbox/mapbox-gl-js/pull/10968))
## 13.21.0
### ✨ Features and improvements
* Add support for `text-writing-mode` property when using `symbol-placement: line` text labels. ([#10647](https://github.com/mapbox/mapbox-gl-js/pull/10647))
* Note: This change will bring following changes for CJK text block:
* 1. For vertical CJK text, all the characters including Latin and Numbers will be vertically placed now. Previously, Latin and Numbers are horizontally placed.
* 2. For horizontal CJK text, it may have a slight horizontal shift due to the anchor shift.
## 13.20.1
### 🐞 Bug fixes
* Increase strictness of the style API validation for source types ([#10779](https://github.com/mapbox/mapbox-gl-js/pull/10779))
* Remove strictly-increasing requirement for fog range validation ([#10772](https://github.com/mapbox/mapbox-gl-js/pull/10772))
## 13.20.0
### ✨ Features and improvements
* Add configurable fog as a root style specification ([#10564](https://github.com/mapbox/mapbox-gl-js/pull/10564))
* Add support for data-driven expressions in `line-dasharray` and `line-cap` properties. ([#10591](https://github.com/mapbox/mapbox-gl-js/pull/10591))
* Add support for data-driven `text-line-height` ([#10612](https://github.com/mapbox/mapbox-gl-js/pull/10612))
## 13.19.0
### ✨ Features and improvements
* Added array support to minimums and maximums, allowing for validation of multi-dimensional style-spec value constraints. ([#10272](https://github.com/mapbox/mapbox-gl-js/pull/10272))
## 13.18.1
### 🐞 Bug fixes
* Fixed a bug where `map.setStyle` couldn't be used to enable terrain. ([#10177](https://github.com/mapbox/mapbox-gl-js/pull/10177))
## 13.18.0
### ✨ Features and improvements
* Add 3D terrain feature. All layer types and markers can now be extruded using the new `terrain` root level style-spec property or with the function `map.setTerrain()`. ([#1489](https://github.com/mapbox/mapbox-gl-js/issues/1489))
* Add support for unlocked pitch up to 85° (previously 60°). ([#3731](https://github.com/mapbox/mapbox-gl-js/issues/3731))
* Add a new sky layer acting as an infinite background above the horizon line. This layer can be used from the style-spec and has two types: `atmospheric` and `gradient`.
## 13.17.0
### ✨ Features and improvements
* Add a `filter` option for GeoJSON sources to filter out features prior to processing (e.g. before clustering). [#9864](https://github.com/mapbox/mapbox-gl-js/pull/9864)
## 13.16.0
### ✨ Features and improvements
* Added `volatile` source property to control storing the tiles in local storage. ([9702](https://github.com/mapbox/mapbox-gl-js/pull/9702))
* Added `clusterMinPoints` option for clustered GeoJSON sources that defines the minimum number of points to form a cluster. ([#9748](https://github.com/mapbox/mapbox-gl-js/pull/9748))
## 13.15.0
### ✨ Features and improvements
* Add `distance` expression to `style-spec`. This expression returns the shortest distance between a feature and an input geometry ([#9655](https://github.com/mapbox/mapbox-gl-js/pull/9655))
## 13.14.0
### ✨ Features and improvements
* Add `index-of` and `slice` expressions to search arrays and strings for the first occurrence of a specified value and return a section of the original array or string ([#9450](https://github.com/mapbox/mapbox-gl-js/pull/9450)) (h/t [lbutler](https://github.com/lbutler))
## 13.13.1
### ✨ Features and improvements
* Expose `expression.isExpressionFilter(..)` from the bundle. ([#9530](https://github.com/mapbox/mapbox-gl-js/pull/9530))
### 🐛 Bug fixes
* Fix a broken module import where the `style-spec` package was importing files from `mapbox-gl-js`, it's parent repo, causing downstream build systems to break. ([#9522](https://github.com/mapbox/mapbox-gl-js/pull/9522))
## 13.13.0
### ✨ Features and improvements
* Add `within` expression for testing whether an evaluated feature lies within a given GeoJSON object ([#9352](https://github.com/mapbox/mapbox-gl-js/pull/9352)). For example:<br>
`"icon-opacity": ["case", ["==", ["within", "some-polygon"], true], 1,
["==", ["within", "some-polygon"], false], 0]`
* Improve scaling of patterns used in `line-pattern` on all device resolutions and pixel ratios ([#9266](https://github.com/mapbox/mapbox-gl-js/pull/9266))
### 🐛 Bug fixes
* Allow needle argument to `in` expression to be false ([#9295](https://github.com/mapbox/mapbox-gl-js/pull/9295))
* Fix a bug where `icon-image` expression that evaluates to an empty string (`''`) produced a warning ([#9380](https://github.com/mapbox/mapbox-gl-js/pull/9380))
* Prevent exception resulting from `line-dash-array` of empty length ([#9385](https://github.com/mapbox/mapbox-gl-js/pull/9385))
## 13.12.0
* Update `image` expression SDK support table ([#9228](https://github.com/mapbox/mapbox-gl-js/pull/9228))
* Fix `promoteId` for line layers ([#9210](https://github.com/mapbox/mapbox-gl-js/pull/9210), [#9212](https://github.com/mapbox/mapbox-gl-js/pull/9212))
## 13.11.0
### ✨ Features and improvements
* Add `promoteId` option to use a feature property as ID for feature state ([#8987](https://github.com/mapbox/mapbox-gl-js/pull/8987))
* Update `symbol-avoid-edges` documentation regarding global collision detection ([#9157](https://github.com/mapbox/mapbox-gl-js/pull/9157))
* Remove reference to `in` function which has been replaced by the `in` expression ([#9102](https://github.com/mapbox/mapbox-gl-js/pull/9102))
### 🐛 Bug fixes
* Fix bug where `symbol-sort-key` was not used for collisions that crossed tile boundaries ([#9054](https://github.com/mapbox/mapbox-gl-js/pull/9054))
## 13.10.2
### 🐛 Bug fixes
* Fix style validation error messages not being displayed ([#9073](https://github.com/mapbox/mapbox-gl-js/pull/9073))
## 13.10.0
### ✨ Features and improvements
* Add ability to insert images into text labels using an `image` expression within a `format` expression: `"text-field": ["format", "Some text", ["image", "my-image"], "some more text"]` ([#8904](https://github.com/mapbox/mapbox-gl-js/pull/8904))
* Add `in` expression. It can check if a value is in an array (`["in", value, array]`) or a substring is in a string (`["in", substring, string]`) ([#8876](https://github.com/mapbox/mapbox-gl-js/pull/8876))
* Add support for stretchable images (aka nine-part or nine-patch images). Stretchable images can be used with `icon-text-fit` to draw resized images with unstretched corners and borders. ([#8997](https://github.com/mapbox/mapbox-gl-js/pull/8997))
* Add an es modules build of for mapbox-gl-style-spec in dist/ ([#8247](https://github.com/mapbox/mapbox-gl-js/pull/8247)) (h/t [ahocevar](https://github.com/ahocevar))
## 13.9.1
### ✨ Improvement
* Rename `Image` type to `ResolvedImage`, to better represent the result of an `image` expression evaluation. ([#8901](https://github.com/mapbox/mapbox-gl-js/pull/8901))
## 13.9.0
* Add `image` expression operator to determine image availability ([#8684](https://github.com/mapbox/mapbox-gl-js/pull/8684))
* Add a style-spec function to validate that styles are compatible with the Mapbox API ([#8663](https://github.com/mapbox/mapbox-gl-js/pull/8663))
## 13.8.0
- Introduce `text-writing-mode` symbol layer property to allow placing point labels vertically. [#8399](https://github.com/mapbox/mapbox-gl-js/pull/8399)
- Allow `text-color` to be used in formatted expressions to be able to draw different parts of a label in different colors. [#8068](https://github.com/mapbox/mapbox-gl-js/pull/8068)
- Improve conversion of legacy filters with duplicate values. [#8542](https://github.com/mapbox/mapbox-gl-js/pull/8542)
## 13.7.2
### 🐛 Bug fixes
* Fix SDK support spec section for variable label placement ([#8384](https://github.com/mapbox/mapbox-gl-js/pull/8384)) (h/t [@pozdnyakov](https://github.com/pozdnyakov))
### ✨ Features and improvements
* Add SDK support spec section for text-radial-offset ([#8401](https://github.com/mapbox/mapbox-gl-js/pull/8401))
* Add `*-sort-key` layout property for circle, fill, line ([#8467](https://github.com/mapbox/mapbox-gl-js/pull/8467))
* Expose convertFilter API in the style specification ([#8493](https://github.com/mapbox/mapbox-gl-js/pull/8493))
## 13.7.1
### 🐛 Bug fixes
* Fix format expression options validation ([#8339](https://github.com/mapbox/mapbox-gl-js/pull/8339))
* Fix SDK support information for style properties added in v13.7.0: ([#8384](https://github.com/mapbox/mapbox-gl-js/pull/8384))
* Add missing SDK support section for `text-radial-offset` property
* Assign SDK versions for `text-variable-anchor` and `text-justify: auto`
## 13.7.0
### ✨ Features and improvements
* Add `text-radial-offset` style property ([#7596](https://github.com/mapbox/mapbox-gl-js/pull/7596))
* Add `text-variable-anchor` style property ([#7596](https://github.com/mapbox/mapbox-gl-js/pull/7596))
* Add `auto` value to `text-justify` style property ([#7596](https://github.com/mapbox/mapbox-gl-js/pull/7596))
## 13.6.0
### ✨ Features and improvements
* Add `clusterProperties` option for aggregated cluster properties ([#2412](https://github.com/mapbox/mapbox-gl-js/issues/2412), fixed by [#7584](https://github.com/mapbox/mapbox-gl-js/pull/7584))
* Add `number-format` expression ([#7626](https://github.com/mapbox/mapbox-gl-js/pull/7626))
* Add `symbol-sort-key` style property ([#7678](https://github.com/mapbox/mapbox-gl-js/pull/7678))
## 13.5.0
### Features and improvements
* Flattens `all` expressions in converted filters ([#7679](https://github.com/mapbox/mapbox-gl-js/pull/7679))
* Compatibility tables are updated ([#7574](https://github.com/mapbox/mapbox-gl-js/pull/7574))
## 13.4.0
### ✨ Features and improvements
* **Tighten style validation**
* Disallow expressions as stop values ([#7396](https://github.com/mapbox/mapbox-gl-js/pull/7396))
* Disallow `feature-state` expressions in filters ([#7366](https://github.com/mapbox/mapbox-gl-js/pull/7366))
## 13.3.0
### 🐛 Bug fixes
* **Expressions**
* Fix `let` expression stripping expected type during parsing ([#7300](https://github.com/mapbox/mapbox-gl-js/issues/7300), fixed by [#7301](https://github.com/mapbox/mapbox-gl-js/pull/7301))
* Fix superfluous wrapping of literals in `literal` expression ([#7336](https://github.com/mapbox/mapbox-gl-js/issues/7336), fixed by [#7337](https://github.com/mapbox/mapbox-gl-js/pull/7337))
* Allow calling `to-color` on values that are already of type `Color` ([#7260](https://github.com/mapbox/mapbox-gl-js/pull/7260))
* Fix `to-array` for empty arrays (([#7261](https://github.com/mapbox/mapbox-gl-js/pull/7261)))
* Fix identity functions for `text-field` when using formatted text ([#7351](https://github.com/mapbox/mapbox-gl-js/pull/7351))
* Fix coercion of `null` to `0` in `to-number` expression ([#7083](https://github.com/mapbox/mapbox-gl-js/issues/7083), fixed by [#7274](https://github.com/mapbox/mapbox-gl-js/pull/7274))
### ✨ Features and improvements
* Add `fill-extrusion-vertical-gradient` property for controlling shading of fill extrusions ([#5768](https://github.com/mapbox/mapbox-gl-js/issues/5768), fixed by [#6841](https://github.com/mapbox/mapbox-gl-js/pull/6841))
## 13.2.0
### 🐛 Bug fixes
* Update the style-spec's old `gl-style-migrate` script to include conversion of legacy functions and filters to their expression equivalents ([#6927](https://github.com/mapbox/mapbox-gl-js/issues/6927), fixed by [#7095](https://github.com/mapbox/mapbox-gl-js/pull/7095))
### ✨ Features and improvements
* Add `symbol-z-order` symbol layout property to style spec ([#7219](https://github.com/mapbox/mapbox-gl-js/pull/7219))
* Implement data-driven styling support for `*-pattern properties` ([#6289](https://github.com/mapbox/mapbox-gl-js/pull/6289))
## 13.1.1
### 🐛 Bug fixes
* Fix broken module import in mapboxgl-style-spec (v13.0.1) ([#6984](https://github.com/mapbox/mapbox-gl-js/issues/6984), fixed by [#6997](https://api.github.com/repos/mapbox/mapbox-gl-js/pulls/6997))
### ✨ Features and improvements
* Improve formatting for style output ([#7029](https://github.com/mapbox/mapbox-gl-js/pull/7029))
## 13.1.0
### ✨ Features and improvements
* Add `raster-resampling` raster paint property ([#6411](https://github.com/mapbox/mapbox-gl-js/pull/6411)) (h/t [andrewharvey](https://github.com/andrewharvey))
* Add `symbol-placement: line-center` ([#6821](https://github.com/mapbox/mapbox-gl-js/pull/6821))
## 13.0.1
### ⚠️ Breaking changes
* Align implicit type behavior of `match` expressions with with `case/==` ([#6684](https://github.com/mapbox/mapbox-gl-js/pull/6684))
* Update spec so that documentation can automatically capture which functions and expressions can be used with which properties ([#6521](https://github.com/mapbox/mapbox-gl-js/pull/6521))
### ✨ Features and improvements
* Add `feature-state` [#6263](https://github.com/mapbox/mapbox-gl-js/pull/6263)
* Add support for GeoJSON attribution ([#6364](https://github.com/mapbox/mapbox-gl-js/pull/6364)) (h/t [andrewharvey](https://github.com/andrewharvey))
* Upgrade to Flow 0.69 ([#6594](https://github.com/mapbox/mapbox-gl-js/pull/6594))
### 🐛 Bug fixes
* Use named exports for style-spec entrypoint module ([#6601](https://github.com/mapbox/mapbox-gl-js/issues/6601)
## 13.0.0
Malformed package published to NPM.
## 12.0.0
### ⚠️ Breaking changes
* The `"to-string"` expression operator now converts null to an empty string rather than to `"null"`. [#6534](https://github.com/mapbox/mapbox-gl-js/pull/6534)
### ✨ Features and improvements
* 🌈 Add line-gradient property [#6303](https://github.com/mapbox/mapbox-gl-js/pull/6303)
* Add collator expression for controlling case and diacritic sensitivity in string comparisons [#6270](https://github.com/mapbox/mapbox-gl-js/pull/6270)
* Add `is-supported-script` expression for determining if a script is supported. [#6260](https://github.com/mapbox/mapbox-gl-js/pull/6260)
* Add `collator` expression for controlling case and diacritic sensitivity in string comparisons [#6270](https://github.com/mapbox/mapbox-gl-js/pull/6270)
* Add `abs`, `round`, `floor`, and `ceil` expression operators [#6496](https://github.com/mapbox/mapbox-gl-js/pull/6496)
* Add support for Mapzen Terrarium tiles in raster-dem sources [#6110](https://github.com/mapbox/mapbox-gl-js/pull/6110)
### 🐛 Bug fixes
- Fix Rollup build [6575](https://github.com/mapbox/mapbox-gl-js/pull/6575)
## 11.1.1
### 🐛 Bug fixes
* Update SDK support information for `text-font`, and `heatmap-color`, and `hillshade` layer properties.
## 11.1.0
### ✨ Features and improvements
* Avoid use of `new Function` for filters by converting old-style filters to expressions [#5665](https://github.com/mapbox/mapbox-gl-js/pull/5665).
## 11.0.0
### ⚠️ Breaking changes
* Remove constants validating code [#5885](https://github.com/mapbox/mapbox-gl-js/pull/5885)
* `"colorSpace": "hcl"` now uses shortest-path interpolation for hue [#5811](https://github.com/mapbox/mapbox-gl-js/issues/5811)
### ✨ Features and improvements
* Introduce client-side hillshading with `raster-dem` source type and `hillshade` layer type [#5286](https://github.com/mapbox/mapbox-gl-js/pull/5286)
* Add Color#toString and expose Color publicly [#5866](https://github.com/mapbox/mapbox-gl-js/pull/5866)
* Improve typing for `==` and `!=` expressions [#5840](https://github.com/mapbox/mapbox-gl-js/pull/5840)
* Made `coalesce` expressions more useful [#5755](https://github.com/mapbox/mapbox-gl-js/issues/5755)
* Enable implicit type assertions for array types [#5738](https://github.com/mapbox/mapbox-gl-js/pull/5738)
### 🐛 Bug fixes
* Fix standalone browser build [#5736](https://github.com/mapbox/mapbox-gl-js/pull/5736), [#5906](https://github.com/mapbox/mapbox-gl-js/pull/5906)
* Handle NaN as input to step and interpolate expressions [#5757](https://github.com/mapbox/mapbox-gl-js/pull/5757)
* Fix style diffing for changes to GeoJSON sources [#5745](https://github.com/mapbox/mapbox-gl-js/pull/5745)
* Mark layer "type" property as required [#5849](https://github.com/mapbox/mapbox-gl-js/pull/5849)
## 10.1.0
* Remove support for validating and migrating v6 styles [#5604](https://github.com/mapbox/mapbox-gl-js/pull/5604)
* Remove support for validating v7 styles [#5604](https://github.com/mapbox/mapbox-gl-js/pull/5604)
* Remove spaces after commas in `to-string` representation of colors [#5480](https://github.com/mapbox/mapbox-gl-js/pull/5480)
* Fix bugs preventing `mapbox-gl-style-spec` package on NPM from being used externally [#5502](https://github.com/mapbox/mapbox-gl-js/pull/5502)
* Fix flow-typed interface generator [#5478](https://github.com/mapbox/mapbox-gl-js/pull/5478)
* Export `function` [#5584](https://github.com/mapbox/mapbox-gl-js/pull/5584)
* Export `StylePropertySpecification` type [#5593](https://github.com/mapbox/mapbox-gl-js/pull/5593)
* Split the `source_tile` entry in the JSON schema into `source_vector` and `source_raster` [#5604](https://github.com/mapbox/mapbox-gl-js/pull/5604)
### Validation Changes
* Require that `heatmap-color` use expressions instead of stop functions [#5624](https://github.com/mapbox/mapbox-gl-js/issues/5624)
* Remove support for including `{tokens}` in expressions for `text-field` and `icon-image` [#5599](https://github.com/mapbox/mapbox-gl-js/issues/5599)
* Disallow interpolation in expressions for `line-dasharray` [#5519](https://github.com/mapbox/mapbox-gl-js/pull/5519)
* Validate that zoom expressions only appear at the top level of an expression [#5609](https://github.com/mapbox/mapbox-gl-js/issues/5609)
* Validate that `step` and `interpolate` expressions don't have any duplicate stops [#5605](https://github.com/mapbox/mapbox-gl-js/issues/5605)
* Split `curve` expression into `step` and `interpolate` expressions [#5542](https://github.com/mapbox/mapbox-gl-js/pull/5542)
* Validate expression values for enum-typed properties [#5589](https://github.com/mapbox/mapbox-gl-js/pull/5589)
* Improve validation to catch uses of deprecated function syntax [#5667](https://github.com/mapbox/mapbox-gl-js/pull/5667)
## 10.0.1
* Fix bug preventing @mapbox/mapbox-gl-style-spec package from being usable outside of mapbox-gl-js (#5502)
## 10.0.0
* Add expression and heatmap layer support. See Mapbox GL JS v0.40.1 changelog entry for details.
## 9.0.1
* Remove `fast-stable-stringify` dependency (#5152)
## 9.0.0
* Fix validation error on categorical zoom-and-property functions (#4220)
* Relax requirement that styles using "icon-image" must have a "sprite"
* Prevent infinite loop in binarySearchForIndex when duplicate stops are present. (#4503)
* Ensure generated composite function stops are in the correct order (#4509)
* Update SDK support matrices to be current as of GL JS v0.35.1, iOS SDK v3.5.0, Android SDK 5.0.0
* Remove support for implicitly-categorical functions
* BREAKING CHANGE: the API for the `function` module has changed. The `interpolated` and `piecewise-constant` exports
were replaced with a single unnamed export, a function which accepts an object conforming to the style spec "function"
definition, and an object defining a style spec property. It handles color parsing and validation of feature values
internally.
* Functions now support a "default" property.
* `parseColor` was promoted from gl-js.
## 8.11.0
* Merge `feature-filter` repository into this repository #639
## 8.10.0
v8.0.0 styles are fully compatible with v8.10.0.
### Style Specification Changes
* Added `colorSpace` option to functions
* Added `fill-extrusion` layer type
* Add top-level `light` property
* Remove increase maximum `maxzoom` to 24
* Deprecate paint classes :warning:
* Increase strictness of function validation
### Reference JSON & API Changes
* Added `deref` utility
* Added `group_by_layout` utility
* Merge `mapbox-gl-function` repository into this repository
## 8.9.0
v8.0.0 styles are fully compatible with v8.9.0.
* Added identity functions
* Added `auto` value which represents the calculated default value
## 8.8.1
v8.0.0 styles are fully compatible with v8.8.1.
* Fixed style validation for layers with invalid types
## 8.8.0
v8.0.0 styles are fully compatible with v8.8.0.
* Clarified documentation around fill-opacity.
* Update function documentation and validation for property functions.
* Add text-pitch-alignment property.
* Add icon-text-fit, icon-text-fit-padding properties.
## 8.7.0
v8.0.0 styles are fully compatible with v8.7.0.
* Add support for has / !has operators.
## 8.6.0
v8.0.0 styles are fully compatible with v8.6.0.
* Added support for zoom and feature driven functions.
## 8.4.2
v8.0.0 styles are fully compatible with v8.4.2.
* Refactored style validator to expose more granular validation methods
## 8.4.1
v8.0.0 styles are fully compatible with v8.4.1.
* Revert ramp validation checks that broke some styles.
## 8.4.0
v8.0.0 styles are fully compatible with v8.4.0.
* Added `cluster`, `clusterRadius`, `clusterMaxZoom` GeoJSON source properties.
## 8.3.0
v8.0.0 styles are fully compatible with v8.3.0.
* Added `line-offset` style property
## 8.2.1
v8.0.0 styles are fully compatible with v8.2.1.
* Enforce that all layers that use a vector source specify a "source-layer"
## 8.2.0
v8.0.0 styles are fully compatible with v8.2.0.
* Add inline `example` property.
* Enforce that all style properties must have documentation in `doc` property.
* Create minified style specs with `doc` and `example` properties removed.
* `validate` now validates against minified style spec.
* `format` now accepts `space` option to use with `JSON.stringify`.
* Remove `gl-style-spritify`. Mapbox GL sprites are now created automatically by
the Mapbox style APIs, or for hand-crafted styles, by [spritezero-cli](https://github.com/mapbox/spritezero-cli).
## 8.1.0
v8.0.0 styles are fully compatible with v8.1.0.
* [BREAKING] Simplified layout/paint layer property types to more closely align
with v7 types.
* Fixed migration script compatibility with newer versions of Node.js and io.js
* Removed `constants` from schema, they were deprecated in v8
* Added style diff utility to generate semantic deltas between two stylesheets
* Added `visibility` property to `circle` layer type
* Added `pitch` property to stylesheet
## 8.0.0
Introduction of Mapbox GL style specification v8. To migrate a v7 style to v8,
use the `gl-style-migrate` script as described in the README.
* [BREAKING] The value of the `text-font` property is now an array of
strings, rather than a single comma separated string.
* [BREAKING] Renamed `symbol-min-distance` to `symbol-spacing`.
* [BREAKING] Renamed `background-image` to `background-pattern`.
* [BREAKING] Renamed `line-image` to `line-pattern`.
* [BREAKING] Renamed `fill-image` to `fill-pattern`.
* [BREAKING] Renamed the `url` property of the video source type to `urls`.
* [BREAKING] Coordinates in video sources are now specified in [lon, lat] order.
* [BREAKING] Removed `text-max-size` and `icon-max-size` properties; these
are now calculated automatically.
* [BREAKING] `text-size` and `icon-size` are now layout properties instead of paint properties.
* [BREAKING] Constants are no longer supported. If you are editing styles by
hand and want to use constants, you can use a preprocessing step with a tool
like [ScreeSS](https://github.com/screee/screess).
* [BREAKING] The format for `mapbox://` glyphs URLs has changed; you should
now use `mapbox://fonts/mapbox/{fontstack}/{range}.pbf`.
* [BREAKING] Reversed the priority of layers for calculating label placement:
labels for layers that appear later in the style now have priority over earlier
layers.
* Added a new `image` source type.
* Added a new `circle` layer type.
* Default map center location can now be set in the style.
* Added `mapbox://` sprite URLs `mapbox://sprite/{user | "mapbox"}/{id}`
## 7.5.0
* Added gl-style-composite script, for auto-compositing sources in a style.
## 7.4.1
* Use JSON.stringify for formatting instead of js-beautify
## 7.0.0
Introduction of Mapbox GL style specification v7.
* [BREAKING] Improve dashed lines (#234)
* [BREAKING] Remove prerendered layers (#232)
* Explicit visibility property (#212)
* Functions for all properties (#237)
## 6.0.0 (Style spec v6)
Introduction of Mapbox GL style specification v6.
* [BREAKING] New filter syntax (#178)
* [BREAKING] Line gap property (#131)
* [BREAKING] Remove dashes from min/max-zoom (#175)
* [BREAKING] New layout/paint terminology (#166)
* [BREAKING] Single text positioning property (#197)
* Added requirements (#200)
* Added minimum, maximum, and period values (#198)
## 0.0.5 (in progress)
* [BREAKING] Switch to suffix for transition properties (`transition-*` -> `*-transition`).
* Added support for remote, non-Mapbox TileJSON sources.
* [BREAKING] Source `minZoom` and `maxZoom` renamed to `minzoom` and `maxzoom to match TileJSON.
* Added support for `mapbox://` glyph URLs.
* [BREAKING] Renamed `raster-fade` to `raster-fade-duration`.
* Added background-opacity property.
* Added "tokens" property to string values that can autocomplete fields from layers
* Added "units" property to describe value types
## 0.0.4 (Aug 8 2014)
* Initial public release

59
node_modules/@mapbox/mapbox-gl-style-spec/README.md generated vendored Normal file
View File

@@ -0,0 +1,59 @@
# Mapbox GL style specification & utilities
This directory contains code and reference files that define the Mapbox GL style specification and provides some utilities for working with Mapbox styles.
## npm package
The Mapbox GL style specification and utilities are published as a seperate npm
package so that they can be installed without the bulk of GL JS.
npm install @mapbox/mapbox-gl-style-spec
## CLI Tools
If you install this package globally, you will have access to several CLI tools.
npm install @mapbox/mapbox-gl-style-spec --global
### `gl-style-composite`
```bash
$ gl-style-composite style.json
```
Will take a non-composited style and produce a [composite style](https://www.mapbox.com/blog/better-label-placement-in-mapbox-studio/).
### `gl-style-migrate`
This repo contains scripts for migrating GL styles of any version to the latest version
(currently v8). Migrate a style like this:
```bash
$ gl-style-migrate bright-v7.json > bright-v8.json
```
To migrate a file in place, you can use the `sponge` utility from the `moreutils` package:
```bash
$ brew install moreutils
$ gl-style-migrate bright.json | sponge bright.json
```
### `gl-style-format`
```bash
$ gl-style-format style.json
```
Will format the given style JSON to use standard indentation and sorted object keys.
### `gl-style-validate`
```bash
$ gl-style-validate style.json
```
Will validate the given style JSON and print errors to stdout. Provide a
`--json` flag to get JSON output.
To validate that a style can be uploaded to the Mapbox Styles API, use the `--mapbox-api-supported` flag.

View File

@@ -0,0 +1,25 @@
#!/usr/bin/env node
// @flow
/* eslint-disable no-process-exit */
import fs from 'fs';
import minimist from 'minimist';
/* eslint import/no-unresolved: [error, { ignore: ['^@mapbox/mapbox-gl-style-spec$'] }] */
/* $FlowFixMe[cannot-resolve-module] */
import {format, composite} from '@mapbox/mapbox-gl-style-spec';
const argv = minimist(process.argv.slice(2));
if (argv.help || argv.h || (!argv._.length && process.stdin.isTTY)) {
help();
process.exit(0);
}
console.log(format(composite(JSON.parse(fs.readFileSync(argv._[0]).toString()))));
function help() {
console.log('usage:');
console.log(' gl-style-composite style.json');
}

View File

@@ -0,0 +1,30 @@
#!/usr/bin/env node
// @flow
/* eslint-disable no-process-exit */
import fs from 'fs';
import minimist from 'minimist';
/* eslint import/no-unresolved: [error, { ignore: ['^@mapbox/mapbox-gl-style-spec$'] }] */
/* $FlowFixMe[cannot-resolve-module] */
import {format} from '@mapbox/mapbox-gl-style-spec';
const argv = minimist(process.argv.slice(2));
if (argv.help || argv.h || (!argv._.length && process.stdin.isTTY)) {
help();
process.exit(0);
}
console.log(format(JSON.parse(fs.readFileSync(argv._[0]).toString()), argv.space));
function help() {
console.log('usage:');
console.log(' gl-style-format source.json > destination.json');
console.log('');
console.log('options:');
console.log(' --space <num>');
console.log(' Number of spaces in output (default "2")');
console.log(' Pass "0" for minified output.');
}

View File

@@ -0,0 +1,25 @@
#!/usr/bin/env node
// @flow
/* eslint-disable no-process-exit */
import fs from 'fs';
import minimist from 'minimist';
/* eslint import/no-unresolved: [error, { ignore: ['^@mapbox/mapbox-gl-style-spec$'] }] */
/* $FlowFixMe[cannot-resolve-module] */
import {format, migrate} from '@mapbox/mapbox-gl-style-spec';
const argv = minimist(process.argv.slice(2));
if (argv.help || argv.h || (!argv._.length && process.stdin.isTTY)) {
help();
process.exit(0);
}
console.log(format(migrate(JSON.parse(fs.readFileSync(argv._[0]).toString()))));
function help() {
console.log('usage:');
console.log(' gl-style-migrate source.json > destination.json');
}

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env node
// @flow
/* eslint-disable no-process-exit */
import rw from 'rw';
import minimist from 'minimist';
/* eslint import/no-unresolved: [error, { ignore: ['^@mapbox/mapbox-gl-style-spec$'] }] */
/* $FlowFixMe[cannot-resolve-module] */
import {validate, validateMapboxApiSupported} from '@mapbox/mapbox-gl-style-spec';
const argv = minimist(process.argv.slice(2), {
boolean: ['json', 'mapbox-api-supported'],
});
let status = 0;
if (argv.help || argv.h || (!argv._.length && process.stdin.isTTY)) {
help();
process.exit(status);
}
if (!argv._.length) {
argv._.push('/dev/stdin');
}
argv._.forEach((file) => {
let errors = [];
if (argv['mapbox-api-supported']) {
errors = validateMapboxApiSupported(rw.readFileSync(file, 'utf8'));
} else {
errors = validate(rw.readFileSync(file, 'utf8'));
}
if (errors.length) {
if (argv.json) {
process.stdout.write(JSON.stringify(errors, null, 2));
} else {
errors.forEach((e) => {
console.log('%s:%d: %s', file, e.line, e.message);
});
}
status = 1;
}
});
process.exit(status);
function help() {
console.log('usage:');
console.log(' gl-style-validate file.json');
console.log(' gl-style-validate < file.json');
console.log('');
console.log('options:');
console.log('--json output errors as json');
console.log('--mapbox-api-supported validate compatibility with Mapbox Styles API');
}

View File

50
node_modules/@mapbox/mapbox-gl-style-spec/composite.js generated vendored Normal file
View File

@@ -0,0 +1,50 @@
export default function (style) {
const styleIDs = [];
const sourceIDs = [];
const compositedSourceLayers = [];
for (const id in style.sources) {
const source = style.sources[id];
if (source.type !== "vector")
continue;
const match = /^mapbox:\/\/(.*)/.exec(source.url);
if (!match)
continue;
styleIDs.push(id);
sourceIDs.push(match[1]);
}
if (styleIDs.length < 2)
return style;
styleIDs.forEach((id) => {
delete style.sources[id];
});
const compositeID = sourceIDs.join(",");
style.sources[compositeID] = {
"type": "vector",
"url": `mapbox://${compositeID}`
};
style.layers.forEach((layer) => {
if (styleIDs.indexOf(layer.source) >= 0) {
layer.source = compositeID;
if ('source-layer' in layer) {
if (compositedSourceLayers.indexOf(layer['source-layer']) >= 0) {
throw new Error('Conflicting source layer names');
} else {
compositedSourceLayers.push(layer['source-layer']);
}
}
}
});
return style;
}

53
node_modules/@mapbox/mapbox-gl-style-spec/deref.js generated vendored Normal file
View File

@@ -0,0 +1,53 @@
// @flow
import refProperties from './util/ref_properties.js';
import type {LayerSpecification} from './types.js';
function deref(layer: LayerSpecification, parent: LayerSpecification): LayerSpecification {
const result = {};
for (const k in layer) {
if (k !== 'ref') {
result[k] = layer[k];
}
}
refProperties.forEach((k) => {
if (k in parent) {
result[k] = (parent: any)[k];
}
});
return ((result: any): LayerSpecification);
}
/**
* Given an array of layers, some of which may contain `ref` properties
* whose value is the `id` of another property, return 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.
*
* The input is not modified. The output may contain references to portions
* of the input.
*
* @private
* @param {Array<Layer>} layers
* @returns {Array<Layer>}
*/
export default function derefLayers(layers: Array<LayerSpecification>): Array<LayerSpecification> {
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]: any).ref]);
}
}
return layers;
}

434
node_modules/@mapbox/mapbox-gl-style-spec/diff.js generated vendored Normal file
View File

@@ -0,0 +1,434 @@
// @flow
import isEqual from './util/deep_equal.js';
import type {StyleSpecification} from './types.js';
type Command = {
command: string;
args: Array<any>;
};
export const operations: {[_: string]: string} = {
/*
* { command: 'setStyle', args: [stylesheet] }
*/
setStyle: 'setStyle',
/*
* { command: 'addLayer', args: [layer, 'beforeLayerId'] }
*/
addLayer: 'addLayer',
/*
* { command: 'removeLayer', args: ['layerId'] }
*/
removeLayer: 'removeLayer',
/*
* { command: 'setPaintProperty', args: ['layerId', 'prop', value] }
*/
setPaintProperty: 'setPaintProperty',
/*
* { command: 'setLayoutProperty', args: ['layerId', 'prop', value] }
*/
setLayoutProperty: 'setLayoutProperty',
/*
* { command: 'setFilter', args: ['layerId', filter] }
*/
setFilter: 'setFilter',
/*
* { command: 'addSource', args: ['sourceId', source] }
*/
addSource: 'addSource',
/*
* { command: 'removeSource', args: ['sourceId'] }
*/
removeSource: 'removeSource',
/*
* { command: 'setGeoJSONSourceData', args: ['sourceId', data] }
*/
setGeoJSONSourceData: 'setGeoJSONSourceData',
/*
* { command: 'setLayerZoomRange', args: ['layerId', 0, 22] }
*/
setLayerZoomRange: 'setLayerZoomRange',
/*
* { command: 'setLayerProperty', args: ['layerId', 'prop', value] }
*/
setLayerProperty: 'setLayerProperty',
/*
* { command: 'setCenter', args: [[lon, lat]] }
*/
setCenter: 'setCenter',
/*
* { command: 'setZoom', args: [zoom] }
*/
setZoom: 'setZoom',
/*
* { command: 'setBearing', args: [bearing] }
*/
setBearing: 'setBearing',
/*
* { command: 'setPitch', args: [pitch] }
*/
setPitch: 'setPitch',
/*
* { command: 'setSprite', args: ['spriteUrl'] }
*/
setSprite: 'setSprite',
/*
* { command: 'setGlyphs', args: ['glyphsUrl'] }
*/
setGlyphs: 'setGlyphs',
/*
* { command: 'setTransition', args: [transition] }
*/
setTransition: 'setTransition',
/*
* { command: 'setLighting', args: [lightProperties] }
*/
setLight: 'setLight',
/*
* { command: 'setTerrain', args: [terrainProperties] }
*/
setTerrain: 'setTerrain',
/*
* { command: 'setFog', args: [fogProperties] }
*/
setFog: 'setFog',
/*
* { command: 'setProjection', args: [projectionProperties] }
*/
setProjection: 'setProjection'
};
function addSource(sourceId, after, commands) {
commands.push({command: operations.addSource, args: [sourceId, after[sourceId]]});
}
function removeSource(sourceId, commands, sourcesRemoved) {
commands.push({command: operations.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 (!before[sourceId].hasOwnProperty(prop)) continue;
if (prop !== 'data' && !isEqual(before[sourceId][prop], after[sourceId][prop])) {
return false;
}
}
for (prop in after[sourceId]) {
if (!after[sourceId].hasOwnProperty(prop)) continue;
if (prop !== 'data' && !isEqual(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 (!before.hasOwnProperty(sourceId)) continue;
if (!after.hasOwnProperty(sourceId)) {
removeSource(sourceId, commands, sourcesRemoved);
}
}
// look for sources to add/update
for (sourceId in after) {
if (!after.hasOwnProperty(sourceId)) continue;
if (!before.hasOwnProperty(sourceId)) {
addSource(sourceId, after, commands);
} else if (!isEqual(before[sourceId], after[sourceId])) {
if (before[sourceId].type === 'geojson' && after[sourceId].type === 'geojson' && canUpdateGeoJSON(before, after, sourceId)) {
commands.push({command: operations.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 || {};
let prop;
for (prop in before) {
if (!before.hasOwnProperty(prop)) continue;
if (!isEqual(before[prop], after[prop])) {
commands.push({command, args: [layerId, prop, after[prop], klass]});
}
}
for (prop in after) {
if (!after.hasOwnProperty(prop) || before.hasOwnProperty(prop)) continue;
if (!isEqual(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 i, d, layerId, beforeLayer, afterLayer, insertBeforeLayerId, prop;
// remove layers
for (i = 0, d = 0; i < beforeOrder.length; i++) {
layerId = beforeOrder[i];
if (!afterIndex.hasOwnProperty(layerId)) {
commands.push({command: operations.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 (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 (beforeIndex.hasOwnProperty(layerId)) {
// remove the layer before we insert at the correct position
commands.push({command: operations.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];
commands.push({command: operations.addLayer, args: [afterIndex[layerId], insertBeforeLayerId]});
tracker.splice(tracker.length - i, 0, layerId);
clean[layerId] = true;
}
// update layers
for (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] || isEqual(beforeLayer, afterLayer)) continue;
// If source, source-layer, or type have changes, then remove the layer
// and add it back 'from scratch'.
if (!isEqual(beforeLayer.source, afterLayer.source) || !isEqual(beforeLayer['source-layer'], afterLayer['source-layer']) || !isEqual(beforeLayer.type, afterLayer.type)) {
commands.push({command: operations.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];
commands.push({command: operations.addLayer, args: [afterLayer, insertBeforeLayerId]});
continue;
}
// layout, paint, filter, minzoom, maxzoom
diffLayerPropertyChanges(beforeLayer.layout, afterLayer.layout, commands, layerId, null, operations.setLayoutProperty);
diffLayerPropertyChanges(beforeLayer.paint, afterLayer.paint, commands, layerId, null, operations.setPaintProperty);
if (!isEqual(beforeLayer.filter, afterLayer.filter)) {
commands.push({command: operations.setFilter, args: [layerId, afterLayer.filter]});
}
if (!isEqual(beforeLayer.minzoom, afterLayer.minzoom) || !isEqual(beforeLayer.maxzoom, afterLayer.maxzoom)) {
commands.push({command: operations.setLayerZoomRange, args: [layerId, afterLayer.minzoom, afterLayer.maxzoom]});
}
// handle all other layer props, including paint.*
for (prop in beforeLayer) {
if (!beforeLayer.hasOwnProperty(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), operations.setPaintProperty);
} else if (!isEqual(beforeLayer[prop], afterLayer[prop])) {
commands.push({command: operations.setLayerProperty, args: [layerId, prop, afterLayer[prop]]});
}
}
for (prop in afterLayer) {
if (!afterLayer.hasOwnProperty(prop) || beforeLayer.hasOwnProperty(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), operations.setPaintProperty);
} else if (!isEqual(beforeLayer[prop], afterLayer[prop])) {
commands.push({command: operations.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 mapbox-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
*/
export default function diffStyles(before: StyleSpecification, after: StyleSpecification): Array<Command> {
if (!before) return [{command: operations.setStyle, args: [after]}];
let commands = [];
try {
// Handle changes to top-level properties
if (!isEqual(before.version, after.version)) {
return [{command: operations.setStyle, args: [after]}];
}
if (!isEqual(before.center, after.center)) {
commands.push({command: operations.setCenter, args: [after.center]});
}
if (!isEqual(before.zoom, after.zoom)) {
commands.push({command: operations.setZoom, args: [after.zoom]});
}
if (!isEqual(before.bearing, after.bearing)) {
commands.push({command: operations.setBearing, args: [after.bearing]});
}
if (!isEqual(before.pitch, after.pitch)) {
commands.push({command: operations.setPitch, args: [after.pitch]});
}
if (!isEqual(before.sprite, after.sprite)) {
commands.push({command: operations.setSprite, args: [after.sprite]});
}
if (!isEqual(before.glyphs, after.glyphs)) {
commands.push({command: operations.setGlyphs, args: [after.glyphs]});
}
if (!isEqual(before.transition, after.transition)) {
commands.push({command: operations.setTransition, args: [after.transition]});
}
if (!isEqual(before.light, after.light)) {
commands.push({command: operations.setLight, args: [after.light]});
}
if (!isEqual(before.fog, after.fog)) {
commands.push({command: operations.setFog, args: [after.fog]});
}
if (!isEqual(before.projection, after.projection)) {
commands.push({command: operations.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 (layer.source && sourcesRemoved[layer.source]) {
commands.push({command: operations.removeLayer, args: [layer.id]});
} else {
beforeLayers.push(layer);
}
});
}
// Remove the terrain if the source for that terrain is being removed
let beforeTerrain = before.terrain;
if (beforeTerrain) {
if (sourcesRemoved[beforeTerrain.source]) {
commands.push({command: operations.setTerrain, args: [undefined]});
beforeTerrain = undefined;
}
}
commands = commands.concat(removeOrAddSourceCommands);
// Even though terrain is a top-level property
// Its like a layer in the sense that it depends on a source being present.
if (!isEqual(beforeTerrain, after.terrain)) {
commands.push({command: operations.setTerrain, args: [after.terrain]});
}
// 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: operations.setStyle, args: [after]}];
}
return commands;
}

View File

17636
node_modules/@mapbox/mapbox-gl-style-spec/dist/index.cjs generated vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

17610
node_modules/@mapbox/mapbox-gl-style-spec/dist/index.es.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

10
node_modules/@mapbox/mapbox-gl-style-spec/empty.js generated vendored Normal file
View File

@@ -0,0 +1,10 @@
// @flow
import type {StyleSpecification} from './types.js';
export default function emptyStyle(): StyleSpecification {
return {
version: 8,
layers: [],
sources: {}
};
}

View File

@@ -0,0 +1,16 @@
// @flow
// Note: Do not inherit from Error. It breaks when transpiling to ES5.
export default class ParsingError {
message: string;
error: Error;
line: number;
constructor(error: Error) {
this.error = error;
this.message = error.message;
const match = error.message.match(/line (\d+)/);
this.line = match ? parseInt(match[1], 10) : 0;
}
}

View File

@@ -0,0 +1,18 @@
// @flow
// Note: Do not inherit from Error. It breaks when transpiling to ES5.
export default class ValidationError {
message: string;
identifier: ?string;
line: ?number;
constructor(key: ?string, value: ?{ __line__: number }, message: string, identifier: ?string) {
this.message = (key ? `${key}: ` : '') + message;
if (identifier) this.identifier = identifier;
if (value !== null && value !== undefined && value.__line__) {
this.line = value.__line__;
}
}
}

View File

@@ -0,0 +1,162 @@
// @flow
import {toString} from './types.js';
import ParsingContext from './parsing_context.js';
import EvaluationContext from './evaluation_context.js';
import assert from 'assert';
import type {Expression, ExpressionRegistry} from './expression.js';
import type {Type} from './types.js';
import type {Value} from './values.js';
export type Varargs = {| type: Type |};
type Signature = Array<Type> | Varargs;
type Evaluate = (EvaluationContext, Array<Expression>) => Value;
type Definition = [Type, Signature, Evaluate] |
{|type: Type, overloads: Array<[Signature, Evaluate]>|};
class CompoundExpression implements Expression {
name: string;
type: Type;
_evaluate: Evaluate;
args: Array<Expression>;
static definitions: {[_: string]: Definition };
constructor(name: string, type: Type, evaluate: Evaluate, args: Array<Expression>) {
this.name = name;
this.type = type;
this._evaluate = evaluate;
this.args = args;
}
evaluate(ctx: EvaluationContext): Value {
return this._evaluate(ctx, this.args);
}
eachChild(fn: (_: Expression) => void) {
this.args.forEach(fn);
}
outputDefined(): boolean {
return false;
}
serialize(): Array<mixed> {
return [this.name].concat(this.args.map(arg => arg.serialize()));
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Expression {
const op: string = (args[0]: any);
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: ParsingContext = (null: any);
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, context.path, null, context.scope);
// First parse all the args, potentially coercing to the
// types expected by this overload.
const parsedArgs: Array<Expression> = [];
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);
}
}
assert(!signatureContext || signatureContext.errors.length > 0);
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(toString(parsed.type));
}
context.error(`Expected arguments of type ${signatures}, but found (${actualTypes.join(', ')}) instead.`);
}
return null;
}
static register(
registry: ExpressionRegistry,
definitions: {[_: string]: Definition }
) {
assert(!CompoundExpression.definitions);
CompoundExpression.definitions = definitions;
for (const name in definitions) {
registry[name] = CompoundExpression;
}
}
}
function stringifySignature(signature: Signature): string {
if (Array.isArray(signature)) {
return `(${signature.map(toString).join(', ')})`;
} else {
return `(${toString(signature.type)}...)`;
}
}
export default CompoundExpression;

View File

@@ -0,0 +1,130 @@
// @flow
import assert from 'assert';
import {
ObjectType,
ValueType,
StringType,
NumberType,
BooleanType,
checkSubtype,
toString,
array
} from '../types.js';
import RuntimeError from '../runtime_error.js';
import {typeOf} from '../values.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {Type} from '../types.js';
const types = {
string: StringType,
number: NumberType,
boolean: BooleanType,
object: ObjectType
};
class Assertion implements Expression {
type: Type;
args: Array<Expression>;
constructor(type: Type, args: Array<Expression>) {
this.type = type;
this.args = args;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Expression {
if (args.length < 2)
return context.error(`Expected at least one argument.`);
let i = 1;
let type;
const name: string = (args[0]: any);
if (name === 'array') {
let itemType;
if (args.length > 2) {
const type = args[1];
if (typeof type !== 'string' || !(type in types) || type === 'object')
return context.error('The item type argument of "array" must be one of string, number, boolean', 1);
itemType = types[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 {
assert(types[name], name);
type = types[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: EvaluationContext): any | null {
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 ${toString(this.type)}, but found ${toString(typeOf(value))} instead.`);
}
}
assert(false);
return null;
}
eachChild(fn: (_: Expression) => void) {
this.args.forEach(fn);
}
outputDefined(): boolean {
return this.args.every(arg => arg.outputDefined());
}
serialize(): SerializedExpression {
const type = this.type;
const serialized = [type.kind];
if (type.kind === 'array') {
const itemType = type.itemType;
if (itemType.kind === 'string' ||
itemType.kind === 'number' ||
itemType.kind === 'boolean') {
serialized.push(itemType.kind);
const N = type.N;
if (typeof N === 'number' || this.args.length > 1) {
serialized.push(N);
}
}
}
return serialized.concat(this.args.map(arg => arg.serialize()));
}
}
export default Assertion;

View File

@@ -0,0 +1,70 @@
// @flow
import {array, ValueType, NumberType} from '../types.js';
import RuntimeError from '../runtime_error.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {Type, ArrayType} from '../types.js';
import type {Value} from '../values.js';
class At implements Expression {
type: Type;
index: Expression;
input: Expression;
constructor(type: Type, index: Expression, input: Expression) {
this.type = type;
this.index = index;
this.input = input;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?At {
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: ArrayType = (input.type: any);
return new At(t.itemType, index, input);
}
evaluate(ctx: EvaluationContext): Value {
const index = ((this.index.evaluate(ctx): any): number);
const array = ((this.input.evaluate(ctx): any): Array<Value>);
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: (_: Expression) => void) {
fn(this.index);
fn(this.input);
}
outputDefined(): boolean {
return false;
}
serialize(): SerializedExpression {
return ["at", this.index.serialize(), this.input.serialize()];
}
}
export default At;

View File

@@ -0,0 +1,85 @@
// @flow
import assert from 'assert';
import {BooleanType} from '../types.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {Type} from '../types.js';
type Branches = Array<[Expression, Expression]>;
class Case implements Expression {
type: Type;
branches: Branches;
otherwise: Expression;
constructor(type: Type, branches: Branches, otherwise: Expression) {
this.type = type;
this.branches = branches;
this.otherwise = otherwise;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Case {
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: ?Type;
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;
assert(outputType);
return new Case((outputType: any), branches, otherwise);
}
evaluate(ctx: EvaluationContext): any {
for (const [test, expression] of this.branches) {
if (test.evaluate(ctx)) {
return expression.evaluate(ctx);
}
}
return this.otherwise.evaluate(ctx);
}
eachChild(fn: (_: Expression) => void) {
for (const [test, expression] of this.branches) {
fn(test);
fn(expression);
}
fn(this.otherwise);
}
outputDefined(): boolean {
return this.branches.every(([_, out]) => out.outputDefined()) && this.otherwise.outputDefined();
}
serialize(): SerializedExpression {
const serialized = ["case"];
this.eachChild(child => { serialized.push(child.serialize()); });
return serialized;
}
}
export default Case;

View File

@@ -0,0 +1,95 @@
// @flow
import assert from 'assert';
import {checkSubtype, ValueType} from '../types.js';
import ResolvedImage from '../types/resolved_image.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {Type} from '../types.js';
class Coalesce implements Expression {
type: Type;
args: Array<Expression>;
constructor(type: Type, args: Array<Expression>) {
this.type = type;
this.args = args;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Coalesce {
if (args.length < 2) {
return context.error("Expectected at least one argument.");
}
let outputType: Type = (null: any);
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);
}
assert(outputType);
// 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: any), parsedArgs);
}
evaluate(ctx: EvaluationContext): any | null {
let result = null;
let argCount = 0;
let firstImage;
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 so styleimagemissing can fire
if (result && result instanceof ResolvedImage && !result.available) {
// set to first image
if (!firstImage) {
firstImage = result;
}
result = null;
// if we reach the end, return the first image
if (argCount === this.args.length) {
return firstImage;
}
}
if (result !== null) break;
}
return result;
}
eachChild(fn: (_: Expression) => void) {
this.args.forEach(fn);
}
outputDefined(): boolean {
return this.args.every(arg => arg.outputDefined());
}
serialize(): SerializedExpression {
const serialized = ["coalesce"];
this.eachChild(child => { serialized.push(child.serialize()); });
return serialized;
}
}
export default Coalesce;

View File

@@ -0,0 +1,133 @@
// @flow
import assert from 'assert';
import {BooleanType, ColorType, NumberType, StringType, ValueType} from '../types.js';
import {Color, toString as valueToString, validateRGBA} from '../values.js';
import RuntimeError from '../runtime_error.js';
import Formatted from '../types/formatted.js';
import FormatExpression from '../definitions/format.js';
import ImageExpression from '../definitions/image.js';
import ResolvedImage from '../types/resolved_image.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {Type} from '../types.js';
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 implements Expression {
type: Type;
args: Array<Expression>;
constructor(type: Type, args: Array<Expression>) {
this.type = type;
this.args = args;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Expression {
if (args.length < 2)
return context.error(`Expected at least one argument.`);
const name: string = (args[0]: any);
assert(types[name], name);
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: EvaluationContext): null | boolean | number | string | Color | Formatted | ResolvedImage {
if (this.type.kind === 'boolean') {
return Boolean(this.args[0].evaluate(ctx));
} else if (this.type.kind === '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 rbga 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]: any) / 255, (input[1]: any) / 255, (input[2]: any) / 255, (input[3]: any));
}
}
}
throw new RuntimeError(error || `Could not parse color from value '${typeof input === 'string' ? input : String(JSON.stringify(input))}'`);
} else if (this.type.kind === '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.`);
} else if (this.type.kind === '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)));
} else if (this.type.kind === 'resolvedImage') {
return ResolvedImage.fromString(valueToString(this.args[0].evaluate(ctx)));
} else {
return valueToString(this.args[0].evaluate(ctx));
}
}
eachChild(fn: (_: Expression) => void) {
this.args.forEach(fn);
}
outputDefined(): boolean {
return this.args.every(arg => arg.outputDefined());
}
serialize(): SerializedExpression {
if (this.type.kind === 'formatted') {
return new FormatExpression([{content: this.args[0], scale: null, font: null, textColor: null}]).serialize();
}
if (this.type.kind === 'resolvedImage') {
return new ImageExpression(this.args[0]).serialize();
}
const serialized = [`to-${this.type.kind}`];
this.eachChild(child => { serialized.push(child.serialize()); });
return serialized;
}
}
export default Coercion;

View File

@@ -0,0 +1,78 @@
// @flow
import {StringType, BooleanType, CollatorType} from '../types.js';
import Collator from '../types/collator.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type EvaluationContext from '../evaluation_context.js';
import type ParsingContext from '../parsing_context.js';
import type {Type} from '../types.js';
export default class CollatorExpression implements Expression {
type: Type;
caseSensitive: Expression;
diacriticSensitive: Expression;
locale: Expression | null;
constructor(caseSensitive: Expression, diacriticSensitive: Expression, locale: Expression | null) {
this.type = CollatorType;
this.locale = locale;
this.caseSensitive = caseSensitive;
this.diacriticSensitive = diacriticSensitive;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Expression {
if (args.length !== 2)
return context.error(`Expected one argument.`);
const options = (args[1]: any);
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: EvaluationContext): Collator {
return new Collator(this.caseSensitive.evaluate(ctx), this.diacriticSensitive.evaluate(ctx), this.locale ? this.locale.evaluate(ctx) : null);
}
eachChild(fn: (_: Expression) => void) {
fn(this.caseSensitive);
fn(this.diacriticSensitive);
if (this.locale) {
fn(this.locale);
}
}
outputDefined(): boolean {
// 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;
}
serialize(): SerializedExpression {
const options = {};
options['case-sensitive'] = this.caseSensitive.serialize();
options['diacritic-sensitive'] = this.diacriticSensitive.serialize();
if (this.locale) {
options['locale'] = this.locale.serialize();
}
return ["collator", options];
}
}

View File

@@ -0,0 +1,184 @@
// @flow
import {toString, ValueType, BooleanType, CollatorType} from '../types.js';
import Assertion from './assertion.js';
import {typeOf} from '../values.js';
import RuntimeError from '../runtime_error.js';
import type {Expression, SerializedExpression, ExpressionRegistration} from '../expression.js';
import type EvaluationContext from '../evaluation_context.js';
import type ParsingContext from '../parsing_context.js';
import type {Type} from '../types.js';
type ComparisonOperator = '==' | '!=' | '<' | '>' | '<=' | '>=' ;
function isComparableType(op: ComparisonOperator, type: 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: EvaluationContext, a: any, b: any): boolean { return a === b; }
function neq(ctx: EvaluationContext, a: any, b: any): boolean { return a !== b; }
function lt(ctx: EvaluationContext, a: any, b: any): boolean { return a < b; }
function gt(ctx: EvaluationContext, a: any, b: any): boolean { return a > b; }
function lteq(ctx: EvaluationContext, a: any, b: any): boolean { return a <= b; }
function gteq(ctx: EvaluationContext, a: any, b: any): boolean { return a >= b; }
function eqCollate(ctx: EvaluationContext, a: any, b: any, c: any): boolean { return c.compare(a, b) === 0; }
function neqCollate(ctx: EvaluationContext, a: any, b: any, c: any): boolean { return !eqCollate(ctx, a, b, c); }
function ltCollate(ctx: EvaluationContext, a: any, b: any, c: any): boolean { return c.compare(a, b) < 0; }
function gtCollate(ctx: EvaluationContext, a: any, b: any, c: any): boolean { return c.compare(a, b) > 0; }
function lteqCollate(ctx: EvaluationContext, a: any, b: any, c: any): boolean { return c.compare(a, b) <= 0; }
function gteqCollate(ctx: EvaluationContext, a: any, b: any, c: any): boolean { 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: ComparisonOperator, compareBasic: (EvaluationContext, any, any) => boolean, compareWithCollator: (EvaluationContext, any, any, any) => boolean): ExpressionRegistration {
const isOrderComparison = op !== '==' && op !== '!=';
return class Comparison implements Expression {
type: Type;
lhs: Expression;
rhs: Expression;
collator: ?Expression;
hasUntypedArgument: boolean;
constructor(lhs: Expression, rhs: Expression, collator: ?Expression) {
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: $ReadOnlyArray<mixed>, context: ParsingContext): ?Expression {
if (args.length !== 3 && args.length !== 4)
return context.error(`Expected two or three arguments.`);
const op: ComparisonOperator = (args[0]: any);
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 '${toString(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 '${toString(rhs.type)}'.`);
}
if (
lhs.type.kind !== rhs.type.kind &&
lhs.type.kind !== 'value' &&
rhs.type.kind !== 'value'
) {
return context.error(`Cannot compare types '${toString(lhs.type)}' and '${toString(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: EvaluationContext): boolean {
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: (_: Expression) => void) {
fn(this.lhs);
fn(this.rhs);
if (this.collator) {
fn(this.collator);
}
}
outputDefined(): boolean {
return true;
}
serialize(): SerializedExpression {
const serialized = [op];
this.eachChild(child => { serialized.push(child.serialize()); });
return serialized;
}
};
}
export const Equals: $Call<typeof makeComparison, '==', typeof eq, typeof eqCollate> = makeComparison('==', eq, eqCollate);
export const NotEquals: $Call<typeof makeComparison, '!=', typeof neq, typeof neqCollate> = makeComparison('!=', neq, neqCollate);
export const LessThan: $Call<typeof makeComparison, '<', typeof lt, typeof ltCollate> = makeComparison('<', lt, ltCollate);
export const GreaterThan: $Call<typeof makeComparison, '>', typeof gt, typeof gtCollate> = makeComparison('>', gt, gtCollate);
export const LessThanOrEqual: $Call<typeof makeComparison, '<=', typeof lteq, typeof lteqCollate> = makeComparison('<=', lteq, lteqCollate);
export const GreaterThanOrEqual: $Call<typeof makeComparison, '>=', typeof gteq, typeof gteqCollate> = makeComparison('>=', gteq, gteqCollate);

View File

@@ -0,0 +1,144 @@
// @flow
import {NumberType, ValueType, FormattedType, array, StringType, ColorType, ResolvedImageType} from '../types.js';
import Formatted, {FormattedSection} from '../types/formatted.js';
import {toString, typeOf} from '../values.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type EvaluationContext from '../evaluation_context.js';
import type ParsingContext from '../parsing_context.js';
import type {Type} from '../types.js';
type FormattedSectionExpression = {
// Content of a section may be Image expression or other
// type of expression that is coercable to 'string'.
content: Expression,
scale: Expression | null;
font: Expression | null;
textColor: Expression | null;
}
export default class FormatExpression implements Expression {
type: Type;
sections: Array<FormattedSectionExpression>;
constructor(sections: Array<FormattedSectionExpression>) {
this.type = FormattedType;
this.sections = sections;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Expression {
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: Array<FormattedSectionExpression> = [];
let nextTokenMayBeObject = false;
for (let i = 1; i <= args.length - 1; ++i) {
const arg = (args[i]: any);
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;
}
const lastExpression = sections[sections.length - 1];
lastExpression.scale = scale;
lastExpression.font = font;
lastExpression.textColor = textColor;
} 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});
}
}
return new FormatExpression(sections);
}
evaluate(ctx: EvaluationContext): Formatted {
const evaluateSection = section => {
const evaluatedContent = section.content.evaluate(ctx);
if (typeOf(evaluatedContent) === ResolvedImageType) {
return new FormattedSection('', evaluatedContent, null, null, null);
}
return new FormattedSection(
toString(evaluatedContent),
null,
section.scale ? section.scale.evaluate(ctx) : null,
section.font ? section.font.evaluate(ctx).join(',') : null,
section.textColor ? section.textColor.evaluate(ctx) : null
);
};
return new Formatted(this.sections.map(evaluateSection));
}
eachChild(fn: (_: Expression) => void) {
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);
}
}
}
outputDefined(): boolean {
// Technically the combinatoric set of all children
// Usually, this.text will be undefined anyway
return false;
}
serialize(): SerializedExpression {
const serialized = ["format"];
for (const section of this.sections) {
serialized.push(section.content.serialize());
const options = {};
if (section.scale) {
options['font-scale'] = section.scale.serialize();
}
if (section.font) {
options['text-font'] = section.font.serialize();
}
if (section.textColor) {
options['text-color'] = section.textColor.serialize();
}
serialized.push(options);
}
return serialized;
}
}

View File

@@ -0,0 +1,52 @@
// @flow
import {ResolvedImageType, StringType} from '../types.js';
import ResolvedImage from '../types/resolved_image.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type EvaluationContext from '../evaluation_context.js';
import type ParsingContext from '../parsing_context.js';
import type {Type} from '../types.js';
export default class ImageExpression implements Expression {
type: Type;
input: Expression;
constructor(input: Expression) {
this.type = ResolvedImageType;
this.input = input;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Expression {
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: EvaluationContext): null | ResolvedImage {
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: (_: Expression) => void) {
fn(this.input);
}
outputDefined(): boolean {
// The output of image is determined by the list of available images in the evaluation context
return false;
}
serialize(): SerializedExpression {
return ["image", this.input.serialize()];
}
}

View File

@@ -0,0 +1,72 @@
// @flow
import {BooleanType, StringType, ValueType, NullType, toString, NumberType, isValidType, isValidNativeType} from '../types.js';
import RuntimeError from '../runtime_error.js';
import {typeOf} from '../values.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {Type} from '../types.js';
class In implements Expression {
type: Type;
needle: Expression;
haystack: Expression;
constructor(needle: Expression, haystack: Expression) {
this.type = BooleanType;
this.needle = needle;
this.haystack = haystack;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?In {
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 ${toString(needle.type)} instead`);
}
return new In(needle, haystack);
}
evaluate(ctx: EvaluationContext): boolean {
const needle = (this.needle.evaluate(ctx): any);
const haystack = (this.haystack.evaluate(ctx): any);
if (haystack == null) 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 ${toString(typeOf(needle))} instead.`);
}
if (!isValidNativeType(haystack, ['string', 'array'])) {
throw new RuntimeError(`Expected second argument to be of type array or string, but found ${toString(typeOf(haystack))} instead.`);
}
return haystack.indexOf(needle) >= 0;
}
eachChild(fn: (_: Expression) => void) {
fn(this.needle);
fn(this.haystack);
}
outputDefined(): boolean {
return true;
}
serialize(): SerializedExpression {
return ["in", this.needle.serialize(), this.haystack.serialize()];
}
}
export default In;

View File

@@ -0,0 +1,580 @@
// @flow
import {
type Type,
NumberType,
StringType,
BooleanType,
ColorType,
ObjectType,
ValueType,
ErrorType,
CollatorType,
array,
toString as typeToString
} from '../types.js';
import {typeOf, Color, validateRGBA, toString as valueToString} from '../values.js';
import CompoundExpression from '../compound_expression.js';
import RuntimeError from '../runtime_error.js';
import Let from './let.js';
import Var from './var.js';
import Literal from './literal.js';
import Assertion from './assertion.js';
import Coercion from './coercion.js';
import At from './at.js';
import In from './in.js';
import IndexOf from './index_of.js';
import Match from './match.js';
import Case from './case.js';
import Slice from './slice.js';
import Step from './step.js';
import Interpolate from './interpolate.js';
import Coalesce from './coalesce.js';
import {
Equals,
NotEquals,
LessThan,
GreaterThan,
LessThanOrEqual,
GreaterThanOrEqual
} from './comparison.js';
import CollatorExpression from './collator.js';
import NumberFormat from './number_format.js';
import FormatExpression from './format.js';
import ImageExpression from './image.js';
import Length from './length.js';
import Within from './within.js';
import type {Varargs} from '../compound_expression.js';
import type {ExpressionRegistry} from '../expression.js';
const expressions: ExpressionRegistry = {
// 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
};
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 * alpha, g / 255 * alpha, b / 255 * alpha, alpha);
}
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: Type): Varargs {
return {type};
}
CompoundExpression.register(expressions, {
'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]) => {
return v.evaluate(ctx).toArray();
}
],
'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
],
'pitch': [
NumberType,
[],
(ctx) => ctx.globals.pitch || 0
],
'distance-from-center': [
NumberType,
[],
(ctx) => ctx.distanceFromCenter()
],
'heatmap-density': [
NumberType,
[],
(ctx) => ctx.globals.heatmapDensity || 0
],
'line-progress': [
NumberType,
[],
(ctx) => ctx.globals.lineProgress || 0
],
'sky-radial-progress': [
NumberType,
[],
(ctx) => ctx.globals.skyRadialProgress || 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: any).value] === (v: any).value
],
'filter-id-==': [
BooleanType,
[ValueType],
(ctx, [v]) => ctx.id() === (v: any).value
],
'filter-type-==': [
BooleanType,
[StringType],
(ctx, [v]) => ctx.geometryType() === (v: any).value
],
'filter-<': [
BooleanType,
[StringType, ValueType],
(ctx, [k, v]) => {
const a = ctx.properties()[(k: any).value];
const b = (v: any).value;
return typeof a === typeof b && a < b;
}
],
'filter-id-<': [
BooleanType,
[ValueType],
(ctx, [v]) => {
const a = ctx.id();
const b = (v: any).value;
return typeof a === typeof b && a < b;
}
],
'filter->': [
BooleanType,
[StringType, ValueType],
(ctx, [k, v]) => {
const a = ctx.properties()[(k: any).value];
const b = (v: any).value;
return typeof a === typeof b && a > b;
}
],
'filter-id->': [
BooleanType,
[ValueType],
(ctx, [v]) => {
const a = ctx.id();
const b = (v: any).value;
return typeof a === typeof b && a > b;
}
],
'filter-<=': [
BooleanType,
[StringType, ValueType],
(ctx, [k, v]) => {
const a = ctx.properties()[(k: any).value];
const b = (v: any).value;
return typeof a === typeof b && a <= b;
}
],
'filter-id-<=': [
BooleanType,
[ValueType],
(ctx, [v]) => {
const a = ctx.id();
const b = (v: any).value;
return typeof a === typeof b && a <= b;
}
],
'filter->=': [
BooleanType,
[StringType, ValueType],
(ctx, [k, v]) => {
const a = ctx.properties()[(k: any).value];
const b = (v: any).value;
return typeof a === typeof b && a >= b;
}
],
'filter-id->=': [
BooleanType,
[ValueType],
(ctx, [v]) => {
const a = ctx.id();
const b = (v: any).value;
return typeof a === typeof b && a >= b;
}
],
'filter-has': [
BooleanType,
[ValueType],
(ctx, [k]) => (k: any).value in ctx.properties()
],
'filter-has-id': [
BooleanType,
[],
(ctx) => (ctx.id() !== null && ctx.id() !== undefined)
],
'filter-type-in': [
BooleanType,
[array(StringType)],
(ctx, [v]) => (v: any).value.indexOf(ctx.geometryType()) >= 0
],
'filter-id-in': [
BooleanType,
[array(ValueType)],
(ctx, [v]) => (v: any).value.indexOf(ctx.id()) >= 0
],
'filter-in-small': [
BooleanType,
[StringType, array(ValueType)],
// assumes v is an array literal
(ctx, [k, v]) => (v: any).value.indexOf(ctx.properties()[(k: any).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: any).value], (v: any).value, 0, (v: any).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()
]
});
export default expressions;

View File

@@ -0,0 +1,89 @@
// @flow
import {BooleanType, StringType, ValueType, NullType, toString, NumberType, isValidType, isValidNativeType} from '../types.js';
import RuntimeError from '../runtime_error.js';
import {typeOf} from '../values.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {Type} from '../types.js';
class IndexOf implements Expression {
type: Type;
needle: Expression;
haystack: Expression;
fromIndex: ?Expression;
constructor(needle: Expression, haystack: Expression, fromIndex?: Expression) {
this.type = NumberType;
this.needle = needle;
this.haystack = haystack;
this.fromIndex = fromIndex;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?IndexOf {
if (args.length <= 2 || args.length >= 5) {
return context.error(`Expected 3 or 4 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 ${toString(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: EvaluationContext): any {
const needle = (this.needle.evaluate(ctx): any);
const haystack = (this.haystack.evaluate(ctx): any);
if (!isValidNativeType(needle, ['boolean', 'string', 'number', 'null'])) {
throw new RuntimeError(`Expected first argument to be of type boolean, string, number or null, but found ${toString(typeOf(needle))} instead.`);
}
if (!isValidNativeType(haystack, ['string', 'array'])) {
throw new RuntimeError(`Expected second argument to be of type array or string, but found ${toString(typeOf(haystack))} instead.`);
}
if (this.fromIndex) {
const fromIndex = (this.fromIndex.evaluate(ctx): number);
return haystack.indexOf(needle, fromIndex);
}
return haystack.indexOf(needle);
}
eachChild(fn: (_: Expression) => void) {
fn(this.needle);
fn(this.haystack);
if (this.fromIndex) {
fn(this.fromIndex);
}
}
outputDefined(): boolean {
return false;
}
serialize(): SerializedExpression {
if (this.fromIndex != null && this.fromIndex !== undefined) {
const fromIndex = this.fromIndex.serialize();
return ["index-of", this.needle.serialize(), this.haystack.serialize(), fromIndex];
}
return ["index-of", this.needle.serialize(), this.haystack.serialize()];
}
}
export default IndexOf;

View File

@@ -0,0 +1,268 @@
// @flow
import UnitBezier from '@mapbox/unitbezier';
import * as interpolate from '../../util/interpolate.js';
import {toString, NumberType, ColorType} from '../types.js';
import {findStopLessThanOrEqualTo} from '../stops.js';
import {hcl, lab} from '../../util/color_spaces.js';
import Color from '../../util/color.js';
import type {Stops} from '../stops.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {Type} from '../types.js';
export type InterpolationType =
{ name: 'linear' } |
{ name: 'exponential', base: number } |
{ name: 'cubic-bezier', controlPoints: [number, number, number, number] };
class Interpolate implements Expression {
type: Type;
operator: 'interpolate' | 'interpolate-hcl' | 'interpolate-lab';
interpolation: InterpolationType;
input: Expression;
labels: Array<number>;
outputs: Array<Expression>;
constructor(type: Type, operator: 'interpolate' | 'interpolate-hcl' | 'interpolate-lab', interpolation: InterpolationType, input: Expression, stops: 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: InterpolationType, input: number, lower: number, upper: number): number {
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: $ReadOnlyArray<mixed>, context: ParsingContext): ?Interpolate {
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: any)
};
} 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: Stops = [];
let outputType: Type = (null: any);
if (operator === 'interpolate-hcl' || operator === 'interpolate-lab') {
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 (outputType.kind !== 'number' &&
outputType.kind !== 'color' &&
!(
outputType.kind === 'array' &&
outputType.itemType.kind === 'number' &&
typeof outputType.N === 'number'
)
) {
return context.error(`Type ${toString(outputType)} is not interpolatable.`);
}
return new Interpolate(outputType, (operator: any), interpolation, input, stops);
}
evaluate(ctx: EvaluationContext): Color {
const labels = this.labels;
const outputs = this.outputs;
if (labels.length === 1) {
return outputs[0].evaluate(ctx);
}
const value = ((this.input.evaluate(ctx): any): number);
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);
if (this.operator === 'interpolate') {
return (interpolate[this.type.kind.toLowerCase()]: any)(outputLower, outputUpper, t); // eslint-disable-line import/namespace
} else if (this.operator === 'interpolate-hcl') {
return hcl.reverse(hcl.interpolate(hcl.forward(outputLower), hcl.forward(outputUpper), t));
} else {
return lab.reverse(lab.interpolate(lab.forward(outputLower), lab.forward(outputUpper), t));
}
}
eachChild(fn: (_: Expression) => void) {
fn(this.input);
for (const expression of this.outputs) {
fn(expression);
}
}
outputDefined(): boolean {
return this.outputs.every(out => out.outputDefined());
}
serialize(): SerializedExpression {
let interpolation;
if (this.interpolation.name === 'linear') {
interpolation = ["linear"];
} else if (this.interpolation.name === 'exponential') {
if (this.interpolation.base === 1) {
interpolation = ["linear"];
} else {
interpolation = ["exponential", this.interpolation.base];
}
} else {
interpolation = ["cubic-bezier" ].concat(this.interpolation.controlPoints);
}
const serialized = [this.operator, interpolation, this.input.serialize()];
for (let i = 0; i < this.labels.length; i++) {
serialized.push(
this.labels[i],
this.outputs[i].serialize()
);
}
return serialized;
}
}
/**
* 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);
}
}
export default Interpolate;

View File

@@ -0,0 +1,61 @@
// @flow
import {NumberType, toString} from '../types.js';
import {typeOf} from '../values.js';
import RuntimeError from '../runtime_error.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {Type} from '../types.js';
class Length implements Expression {
type: Type;
input: Expression;
constructor(input: Expression) {
this.type = NumberType;
this.input = input;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Length {
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 ${toString(input.type)} instead.`);
return new Length(input);
}
evaluate(ctx: EvaluationContext): any | number {
const input = this.input.evaluate(ctx);
if (typeof input === 'string') {
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 ${toString(typeOf(input))} instead.`);
}
}
eachChild(fn: (_: Expression) => void) {
fn(this.input);
}
outputDefined(): boolean {
return false;
}
serialize(): SerializedExpression {
const serialized = ["length"];
this.eachChild(child => { serialized.push(child.serialize()); });
return serialized;
}
}
export default Length;

View File

@@ -0,0 +1,72 @@
// @flow
import type {Type} from '../types.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
class Let implements Expression {
type: Type;
bindings: Array<[string, Expression]>;
result: Expression;
constructor(bindings: Array<[string, Expression]>, result: Expression) {
this.type = result.type;
this.bindings = [].concat(bindings);
this.result = result;
}
evaluate(ctx: EvaluationContext): any {
return this.result.evaluate(ctx);
}
eachChild(fn: (_: Expression) => void) {
for (const binding of this.bindings) {
fn(binding[1]);
}
fn(this.result);
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Let {
if (args.length < 4)
return context.error(`Expected at least 3 arguments, but found ${args.length - 1} instead.`);
const bindings: Array<[string, Expression]> = [];
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(): boolean {
return this.result.outputDefined();
}
serialize(): SerializedExpression {
const serialized = ["let"];
for (const [name, expr] of this.bindings) {
serialized.push(name, expr.serialize());
}
serialized.push(this.result.serialize());
return serialized;
}
}
export default Let;

View File

@@ -0,0 +1,77 @@
// @flow
import assert from 'assert';
import {isValue, typeOf, Color} from '../values.js';
import Formatted from '../types/formatted.js';
import type {Type} from '../types.js';
import type {Value} from '../values.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
class Literal implements Expression {
type: Type;
value: Value;
constructor(type: Type, value: Value) {
this.type = type;
this.value = value;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): void | Literal {
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]: any);
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(): Value {
return this.value;
}
eachChild() {}
outputDefined(): boolean {
return true;
}
serialize(): SerializedExpression {
if (this.type.kind === 'array' || this.type.kind === 'object') {
return ["literal", this.value];
} else if (this.value instanceof Color) {
// Constant-folding can generate Literal expressions that you
// couldn't actually generate with a "literal" expression,
// so we have to implement an equivalent serialization here
return ["rgba"].concat(this.value.toArray());
} else if (this.value instanceof Formatted) {
// Same as Color
return this.value.serialize();
} else {
assert(this.value === null ||
typeof this.value === 'string' ||
typeof this.value === 'number' ||
typeof this.value === 'boolean');
return (this.value: any);
}
}
}
export default Literal;

View File

@@ -0,0 +1,158 @@
// @flow
import assert from 'assert';
import {typeOf} from '../values.js';
import {ValueType, type Type} from '../types.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
// Map input label values to output expression index
type Cases = {[number | string]: number};
class Match implements Expression {
type: Type;
inputType: Type;
input: Expression;
cases: Cases;
outputs: Array<Expression>;
otherwise: Expression;
constructor(inputType: Type, outputType: Type, input: Expression, cases: Cases, outputs: Array<Expression>, otherwise: Expression) {
this.inputType = inputType;
this.type = outputType;
this.input = input;
this.cases = cases;
this.outputs = outputs;
this.otherwise = otherwise;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Match {
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;
assert(inputType && outputType);
if (input.type.kind !== 'value' && context.concat(1).checkSubtype((inputType: any), input.type)) {
return null;
}
return new Match((inputType: any), (outputType: any), input, cases, outputs, otherwise);
}
evaluate(ctx: EvaluationContext): any {
const input = (this.input.evaluate(ctx): any);
const output = (typeOf(input) === this.inputType && this.outputs[this.cases[input]]) || this.otherwise;
return output.evaluate(ctx);
}
eachChild(fn: (_: Expression) => void) {
fn(this.input);
this.outputs.forEach(fn);
fn(this.otherwise);
}
outputDefined(): boolean {
return this.outputs.every(out => out.outputDefined()) && this.otherwise.outputDefined();
}
serialize(): SerializedExpression {
const serialized = ["match", this.input.serialize()];
// Sort so serialization has an arbitrary defined order, even though
// branch order doesn't affect evaluation
const sortedLabels = Object.keys(this.cases).sort();
// Group branches by unique match expression to support condensed
// serializations of the form [case1, case2, ...] -> matchExpression
const groupedByOutput: Array<[number, Array<number | string>]> = [];
const outputLookup: {[index: number]: number} = {}; // lookup index into groupedByOutput for a given output expression
for (const label of sortedLabels) {
const outputIndex = outputLookup[this.cases[label]];
if (outputIndex === undefined) {
// First time seeing this output, add it to the end of the grouped list
outputLookup[this.cases[label]] = groupedByOutput.length;
groupedByOutput.push([this.cases[label], [label]]);
} else {
// We've seen this expression before, add the label to that output's group
groupedByOutput[outputIndex][1].push(label);
}
}
const coerceLabel = (label) => this.inputType.kind === 'number' ? Number(label) : label;
for (const [outputIndex, labels] of groupedByOutput) {
if (labels.length === 1) {
// Only a single label matches this output expression
serialized.push(coerceLabel(labels[0]));
} else {
// Array of literal labels pointing to this output expression
serialized.push(labels.map(coerceLabel));
}
serialized.push(this.outputs[outputIndex].serialize());
}
serialized.push(this.otherwise.serialize());
return serialized;
}
}
export default Match;

View File

@@ -0,0 +1,162 @@
// @flow
import {StringType, NumberType} from '../types.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type EvaluationContext from '../evaluation_context.js';
import type ParsingContext from '../parsing_context.js';
import type {Type} from '../types.js';
declare var Intl: {
NumberFormat: Class<Intl$NumberFormat>
};
declare class Intl$NumberFormat {
constructor (
locales?: string | string[],
options?: NumberFormatOptions
): Intl$NumberFormat;
static (
locales?: string | string[],
options?: NumberFormatOptions
): Intl$NumberFormat;
format(a: number): string;
resolvedOptions(): any;
}
type NumberFormatOptions = {
style?: 'decimal' | 'currency' | 'percent' | 'unit';
currency?: null | string;
unit?: null | string;
minimumFractionDigits?: null | string;
maximumFractionDigits?: null | string;
};
export default class NumberFormat implements Expression {
type: Type;
number: Expression;
locale: Expression | null; // BCP 47 language tag
currency: Expression | null; // ISO 4217 currency code, required if style=currency
unit: Expression | null; // Simple units sanctioned for use in ECMAScript, required if style=unit. https://tc39.es/proposal-unified-intl-numberformat/section6/locales-currencies-tz_proposed_out.html#sec-issanctionedsimpleunitidentifier
minFractionDigits: Expression | null; // Default 0
maxFractionDigits: Expression | null; // Default 3
constructor(number: Expression,
locale: Expression | null,
currency: Expression | null,
unit: Expression | null,
minFractionDigits: Expression | null,
maxFractionDigits: Expression | null) {
this.type = StringType;
this.number = number;
this.locale = locale;
this.currency = currency;
this.unit = unit;
this.minFractionDigits = minFractionDigits;
this.maxFractionDigits = maxFractionDigits;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Expression {
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]: any);
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 unit = null;
if (options['unit']) {
unit = context.parse(options['unit'], 1, StringType);
if (!unit) 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, unit, minFractionDigits, maxFractionDigits);
}
evaluate(ctx: EvaluationContext): string {
return new Intl.NumberFormat(this.locale ? this.locale.evaluate(ctx) : [],
{
style:
(this.currency && "currency") ||
(this.unit && "unit") ||
"decimal",
currency: this.currency ? this.currency.evaluate(ctx) : undefined,
unit: this.unit ? this.unit.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: (_: Expression) => void) {
fn(this.number);
if (this.locale) {
fn(this.locale);
}
if (this.currency) {
fn(this.currency);
}
if (this.unit) {
fn(this.unit);
}
if (this.minFractionDigits) {
fn(this.minFractionDigits);
}
if (this.maxFractionDigits) {
fn(this.maxFractionDigits);
}
}
outputDefined(): boolean {
return false;
}
serialize(): SerializedExpression {
const options = {};
if (this.locale) {
options['locale'] = this.locale.serialize();
}
if (this.currency) {
options['currency'] = this.currency.serialize();
}
if (this.unit) {
options['unit'] = this.unit.serialize();
}
if (this.minFractionDigits) {
options['min-fraction-digits'] = this.minFractionDigits.serialize();
}
if (this.maxFractionDigits) {
options['max-fraction-digits'] = this.maxFractionDigits.serialize();
}
return ["number-format", this.number.serialize(), options];
}
}

View File

@@ -0,0 +1,86 @@
// @flow
import {ValueType, NumberType, StringType, array, toString, isValidType, isValidNativeType} from '../types.js';
import RuntimeError from '../runtime_error.js';
import {typeOf} from '../values.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {Type} from '../types.js';
class Slice implements Expression {
type: Type;
input: Expression;
beginIndex: Expression;
endIndex: ?Expression;
constructor(type: Type, input: Expression, beginIndex: Expression, endIndex?: Expression) {
this.type = type;
this.input = input;
this.beginIndex = beginIndex;
this.endIndex = endIndex;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Slice {
if (args.length <= 2 || args.length >= 5) {
return context.error(`Expected 3 or 4 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 ${toString(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: EvaluationContext): any {
const input = (this.input.evaluate(ctx): any);
const beginIndex = (this.beginIndex.evaluate(ctx): number);
if (!isValidNativeType(input, ['string', 'array'])) {
throw new RuntimeError(`Expected first argument to be of type array or string, but found ${toString(typeOf(input))} instead.`);
}
if (this.endIndex) {
const endIndex = (this.endIndex.evaluate(ctx): number);
return input.slice(beginIndex, endIndex);
}
return input.slice(beginIndex);
}
eachChild(fn: (_: Expression) => void) {
fn(this.input);
fn(this.beginIndex);
if (this.endIndex) {
fn(this.endIndex);
}
}
outputDefined(): boolean {
return false;
}
serialize(): SerializedExpression {
if (this.endIndex != null && this.endIndex !== undefined) {
const endIndex = this.endIndex.serialize();
return ["slice", this.input.serialize(), this.beginIndex.serialize(), endIndex];
}
return ["slice", this.input.serialize(), this.beginIndex.serialize()];
}
}
export default Slice;

View File

@@ -0,0 +1,120 @@
// @flow
import {NumberType} from '../types.js';
import {findStopLessThanOrEqualTo} from '../stops.js';
import type {Stops} from '../stops.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {Type} from '../types.js';
class Step implements Expression {
type: Type;
input: Expression;
labels: Array<number>;
outputs: Array<Expression>;
constructor(type: Type, input: Expression, stops: 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: $ReadOnlyArray<mixed>, context: ParsingContext): ?Step {
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: Stops = [];
let outputType: Type = (null: any);
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: EvaluationContext): any {
const labels = this.labels;
const outputs = this.outputs;
if (labels.length === 1) {
return outputs[0].evaluate(ctx);
}
const value = ((this.input.evaluate(ctx): any): number);
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: (_: Expression) => void) {
fn(this.input);
for (const expression of this.outputs) {
fn(expression);
}
}
outputDefined(): boolean {
return this.outputs.every(out => out.outputDefined());
}
serialize(): SerializedExpression {
const serialized = ["step", this.input.serialize()];
for (let i = 0; i < this.labels.length; i++) {
if (i > 0) {
serialized.push(this.labels[i]);
}
serialized.push(this.outputs[i].serialize());
}
return serialized;
}
}
export default Step;

View File

@@ -0,0 +1,46 @@
// @flow
import type {Type} from '../types.js';
import type {Expression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
class Var implements Expression {
type: Type;
name: string;
boundExpression: Expression;
constructor(name: string, boundExpression: Expression) {
this.type = boundExpression.type;
this.name = name;
this.boundExpression = boundExpression;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): void | Var {
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: EvaluationContext): any {
return this.boundExpression.evaluate(ctx);
}
eachChild() {}
outputDefined(): boolean {
return false;
}
serialize(): Array<string> {
return ["var", this.name];
}
}
export default Var;

View File

@@ -0,0 +1,349 @@
// @flow
import {isValue} from '../values.js';
import type {Type} from '../types.js';
import {BooleanType} from '../types.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {GeoJSON, GeoJSONPolygon, GeoJSONMultiPolygon} from '@mapbox/geojson-types';
import type {CanonicalTileID} from '../../../source/tile_id.js';
type GeoJSONPolygons =| GeoJSONPolygon | GeoJSONMultiPolygon;
// minX, minY, maxX, maxY
type BBox = [number, number, number, number];
const EXTENT = 8192;
function updateBBox(bbox: BBox, coord: [number, number]) {
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 mercatorXfromLng(lng: number) {
return (180 + lng) / 360;
}
function mercatorYfromLat(lat: number) {
return (180 - (180 / Math.PI * Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360)))) / 360;
}
function boxWithinBox(bbox1: BBox, bbox2: BBox) {
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 getTileCoordinates(p, canonical: CanonicalTileID) {
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 onBoundary(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);
}
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]);
}
// ray casting algorithm for detecting if point is in polygon
function pointWithinPolygon(point, rings) {
let inside = false;
for (let i = 0, len = rings.length; i < len; i++) {
const ring = rings[i];
for (let j = 0, len2 = ring.length; j < len2 - 1; j++) {
if (onBoundary(point, ring[j], ring[j + 1])) return false;
if (rayIntersect(point, ring[j], ring[j + 1])) inside = !inside;
}
}
return inside;
}
function pointWithinPolygons(point, polygons) {
for (let i = 0; i < polygons.length; i++) {
if (pointWithinPolygon(point, polygons[i])) 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;
}
// a, b are end points for line segment1, c and d are end points for line segment2
function lineIntersectLine(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 (lineIntersectLine(p1, p2, ring[j], ring[j + 1])) {
return true;
}
}
}
return false;
}
function lineStringWithinPolygon(line, polygon) {
// First, check if geometry points of line segments are all inside polygon
for (let i = 0; i < line.length; ++i) {
if (!pointWithinPolygon(line[i], 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 (let i = 0; i < polygons.length; i++) {
if (lineStringWithinPolygon(line, polygons[i])) return true;
}
return false;
}
function getTilePolygon(coordinates, bbox: BBox, canonical: CanonicalTileID) {
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: CanonicalTileID) {
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: CanonicalTileID) {
const worldSize = Math.pow(2, canonical.z) * EXTENT;
const shifts = [canonical.x * EXTENT, canonical.y * EXTENT];
const tilePoints = [];
if (!geometry) return 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: CanonicalTileID) {
const worldSize = Math.pow(2, canonical.z) * EXTENT;
const shifts = [canonical.x * EXTENT, canonical.y * EXTENT];
const tileLines = [];
if (!geometry) return 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: EvaluationContext, polygonGeometry: GeoJSONPolygons) {
const pointBBox = [Infinity, Infinity, -Infinity, -Infinity];
const polyBBox = [Infinity, Infinity, -Infinity, -Infinity];
const canonical = ctx.canonicalID();
if (!canonical) {
return false;
}
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: EvaluationContext, polygonGeometry: GeoJSONPolygons) {
const lineBBox = [Infinity, Infinity, -Infinity, -Infinity];
const polyBBox = [Infinity, Infinity, -Infinity, -Infinity];
const canonical = ctx.canonicalID();
if (!canonical) {
return false;
}
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 implements Expression {
type: Type;
geojson: GeoJSON
geometries: GeoJSONPolygons;
constructor(geojson: GeoJSON, geometries: GeoJSONPolygons) {
this.type = BooleanType;
this.geojson = geojson;
this.geometries = geometries;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Within {
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]: Object);
if (geojson.type === 'FeatureCollection') {
for (let i = 0; i < geojson.features.length; ++i) {
const type = geojson.features[i].geometry.type;
if (type === 'Polygon' || type === 'MultiPolygon') {
return new Within(geojson, geojson.features[i].geometry);
}
}
} 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: EvaluationContext): boolean {
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(): boolean {
return true;
}
serialize(): SerializedExpression {
return ["within", this.geojson];
}
}
export default Within;

View File

@@ -0,0 +1,89 @@
// @flow
import {Color} from './values.js';
import type Point from '@mapbox/point-geometry';
import type {FormattedSection} from './types/formatted.js';
import type {GlobalProperties, Feature, FeatureState} from './index.js';
import type {CanonicalTileID} from '../../source/tile_id.js';
import type {FeatureDistanceData} from '../feature_filter/index.js';
const geometryTypes = ['Unknown', 'Point', 'LineString', 'Polygon'];
class EvaluationContext {
globals: GlobalProperties;
feature: ?Feature;
featureState: ?FeatureState;
formattedSection: ?FormattedSection;
availableImages: ?Array<string>;
canonical: null | CanonicalTileID;
featureTileCoord: ?Point;
featureDistanceData: ?FeatureDistanceData;
_parseColorCache: {[_: string]: ?Color};
constructor() {
this.globals = (null: any);
this.feature = null;
this.featureState = null;
this.formattedSection = null;
this._parseColorCache = {};
this.availableImages = null;
this.canonical = null;
this.featureTileCoord = null;
this.featureDistanceData = null;
}
id(): number | null {
return this.feature && this.feature.id !== undefined ? this.feature.id : null;
}
geometryType(): null | string {
return this.feature ? typeof this.feature.type === 'number' ? geometryTypes[this.feature.type] : this.feature.type : null;
}
geometry(): ?Array<Array<Point>> {
return this.feature && 'geometry' in this.feature ? this.feature.geometry : null;
}
canonicalID(): null | CanonicalTileID {
return this.canonical;
}
properties(): {[string]: any} {
return (this.feature && this.feature.properties) || {};
}
distanceFromCenter(): number {
if (this.featureTileCoord && this.featureDistanceData) {
const c = this.featureDistanceData.center;
const scale = this.featureDistanceData.scale;
const {x, y} = this.featureTileCoord;
// Calculate the distance vector `d` (left handed)
const dX = x * scale - c[0];
const dY = y * scale - c[1];
// The bearing vector `b` (left handed)
const bX = this.featureDistanceData.bearing[0];
const bY = this.featureDistanceData.bearing[1];
// Distance is calculated as `dot(d, v)`
const dist = (bX * dX + bY * dY);
return dist;
}
return 0;
}
parseColor(input: string): ?Color {
let cached = this._parseColorCache[input];
if (!cached) {
cached = this._parseColorCache[input] = Color.parse(input);
}
return cached;
}
}
export default EvaluationContext;

View File

@@ -0,0 +1,27 @@
// @flow
import type {Type} from './types.js';
import type ParsingContext from './parsing_context.js';
import type EvaluationContext from './evaluation_context.js';
export type SerializedExpression = Array<mixed> | Array<string> | string | number | boolean | null;
export interface Expression {
+type: Type;
evaluate(ctx: EvaluationContext): any;
eachChild(fn: Expression => void): void;
/**
* Statically analyze the expression, attempting to enumerate possible outputs. Returns
* false if the complete set of outputs is statically undecidable, otherwise true.
*/
outputDefined(): boolean;
serialize(): SerializedExpression;
}
export type ExpressionParser = (args: $ReadOnlyArray<mixed>, context: ParsingContext) => ?Expression;
export type ExpressionRegistration = Class<Expression> & { +parse: ExpressionParser };
export type ExpressionRegistry = {[_: string]: ExpressionRegistration};

View File

@@ -0,0 +1,399 @@
// @flow
import assert from 'assert';
import extend from '../util/extend.js';
import ParsingError from './parsing_error.js';
import ParsingContext from './parsing_context.js';
import EvaluationContext from './evaluation_context.js';
import CompoundExpression from './compound_expression.js';
import Step from './definitions/step.js';
import Interpolate from './definitions/interpolate.js';
import Coalesce from './definitions/coalesce.js';
import Let from './definitions/let.js';
import definitions from './definitions/index.js';
import * as isConstant from './is_constant.js';
import RuntimeError from './runtime_error.js';
import {success, error} from '../util/result.js';
import {supportsPropertyExpression, supportsZoomExpression, supportsInterpolation} from '../util/properties.js';
import type {Type, EvaluationKind} from './types.js';
import type {Value} from './values.js';
import type {Expression} from './expression.js';
import type {StylePropertySpecification} from '../style-spec.js';
import type {Result} from '../util/result.js';
import type {InterpolationType} from './definitions/interpolate.js';
import type {PropertyValueSpecification} from '../types.js';
import type {FormattedSection} from './types/formatted.js';
import type Point from '@mapbox/point-geometry';
import type {CanonicalTileID} from '../../source/tile_id.js';
import type {FeatureDistanceData} from '../feature_filter/index.js';
export type Feature = {
+type: 1 | 2 | 3 | 'Unknown' | 'Point' | 'LineString' | 'Polygon',
+id?: number | null,
+properties: {[_: string]: any},
+patterns?: {[_: string]: string},
+geometry?: Array<Array<Point>>
};
export type FeatureState = {[_: string]: any};
export type GlobalProperties = $ReadOnly<{
zoom: number,
pitch?: number,
heatmapDensity?: number,
lineProgress?: number,
skyRadialProgress?: number,
isSupportedScript?: (_: string) => boolean,
accumulated?: Value
}>;
export class StyleExpression {
expression: Expression;
_evaluator: EvaluationContext;
_defaultValue: Value;
_warningHistory: {[key: string]: boolean};
_enumValues: ?{[_: string]: any};
constructor(expression: Expression, propertySpec: ?StylePropertySpecification) {
this.expression = expression;
this._warningHistory = {};
this._evaluator = new EvaluationContext();
this._defaultValue = propertySpec ? getDefaultValue(propertySpec) : null;
this._enumValues = propertySpec && propertySpec.type === 'enum' ? propertySpec.values : null;
}
evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection, featureTileCoord?: Point, featureDistanceData?: FeatureDistanceData): any {
this._evaluator.globals = globals;
this._evaluator.feature = feature;
this._evaluator.featureState = featureState;
this._evaluator.canonical = canonical || null;
this._evaluator.availableImages = availableImages || null;
this._evaluator.formattedSection = formattedSection;
this._evaluator.featureTileCoord = featureTileCoord || null;
this._evaluator.featureDistanceData = featureDistanceData || null;
return this.expression.evaluate(this._evaluator);
}
evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection, featureTileCoord?: Point, featureDistanceData?: FeatureDistanceData): any {
this._evaluator.globals = globals;
this._evaluator.feature = feature || null;
this._evaluator.featureState = featureState || null;
this._evaluator.canonical = canonical || null;
this._evaluator.availableImages = availableImages || null;
this._evaluator.formattedSection = formattedSection || null;
this._evaluator.featureTileCoord = featureTileCoord || null;
this._evaluator.featureDistanceData = featureDistanceData || null;
try {
const val = this.expression.evaluate(this._evaluator);
// eslint-disable-next-line no-self-compare
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;
}
}
}
export function isExpression(expression: mixed): boolean {
return Array.isArray(expression) && expression.length > 0 &&
typeof expression[0] === 'string' && expression[0] in definitions;
}
/**
* 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
*/
export function createExpression(expression: mixed, propertySpec: ?StylePropertySpecification): Result<StyleExpression, Array<ParsingError>> {
const parser = new ParsingContext(definitions, [], 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) {
assert(parser.errors.length > 0);
return error(parser.errors);
}
return success(new StyleExpression(parsed, propertySpec));
}
export class ZoomConstantExpression<Kind: EvaluationKind> {
kind: Kind;
isStateDependent: boolean;
_styleExpression: StyleExpression;
constructor(kind: Kind, expression: StyleExpression) {
this.kind = kind;
this._styleExpression = expression;
this.isStateDependent = kind !== ('constant': EvaluationKind) && !isConstant.isStateConstant(expression.expression);
}
evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection): any {
return this._styleExpression.evaluateWithoutErrorHandling(globals, feature, featureState, canonical, availableImages, formattedSection);
}
evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection): any {
return this._styleExpression.evaluate(globals, feature, featureState, canonical, availableImages, formattedSection);
}
}
export class ZoomDependentExpression<Kind: EvaluationKind> {
kind: Kind;
zoomStops: Array<number>;
isStateDependent: boolean;
_styleExpression: StyleExpression;
interpolationType: ?InterpolationType;
constructor(kind: Kind, expression: StyleExpression, zoomStops: Array<number>, interpolationType?: InterpolationType) {
this.kind = kind;
this.zoomStops = zoomStops;
this._styleExpression = expression;
this.isStateDependent = kind !== ('camera': EvaluationKind) && !isConstant.isStateConstant(expression.expression);
this.interpolationType = interpolationType;
}
evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection): any {
return this._styleExpression.evaluateWithoutErrorHandling(globals, feature, featureState, canonical, availableImages, formattedSection);
}
evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection): any {
return this._styleExpression.evaluate(globals, feature, featureState, canonical, availableImages, formattedSection);
}
interpolationFactor(input: number, lower: number, upper: number): number {
if (this.interpolationType) {
return Interpolate.interpolationFactor(this.interpolationType, input, lower, upper);
} else {
return 0;
}
}
}
export type ConstantExpression = {
kind: 'constant',
+evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>) => any,
}
export type SourceExpression = {
kind: 'source',
isStateDependent: boolean,
+evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection) => any,
};
export type CameraExpression = {
kind: 'camera',
+evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>) => any,
+interpolationFactor: (input: number, lower: number, upper: number) => number,
zoomStops: Array<number>,
interpolationType: ?InterpolationType
};
export type CompositeExpression = {
kind: 'composite',
isStateDependent: boolean,
+evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection) => any,
+interpolationFactor: (input: number, lower: number, upper: number) => number,
zoomStops: Array<number>,
interpolationType: ?InterpolationType
};
export type StylePropertyExpression =
| ConstantExpression
| SourceExpression
| CameraExpression
| CompositeExpression;
export function createPropertyExpression(expression: mixed, propertySpec: StylePropertySpecification): Result<StylePropertyExpression, Array<ParsingError>> {
expression = createExpression(expression, propertySpec);
if (expression.result === 'error') {
return expression;
}
const parsed = expression.value.expression;
const isFeatureConstant = isConstant.isFeatureConstant(parsed);
if (!isFeatureConstant && !supportsPropertyExpression(propertySpec)) {
return error([new ParsingError('', 'data expressions not supported')]);
}
const isZoomConstant = isConstant.isGlobalPropertyConstant(parsed, ['zoom', 'pitch', 'distance-from-center']);
if (!isZoomConstant && !supportsZoomExpression(propertySpec)) {
return error([new ParsingError('', 'zoom expressions not supported')]);
}
const zoomCurve = findZoomCurve(parsed);
if (!zoomCurve && !isZoomConstant) {
return error([new ParsingError('', '"zoom" expression may only be used as input to a top-level "step" or "interpolate" expression.')]);
} else if (zoomCurve instanceof ParsingError) {
return error([zoomCurve]);
} else if (zoomCurve instanceof Interpolate && !supportsInterpolation(propertySpec)) {
return error([new ParsingError('', '"interpolate" expressions cannot be used with this property')]);
}
if (!zoomCurve) {
return success(isFeatureConstant ?
(new ZoomConstantExpression('constant', expression.value): ConstantExpression) :
(new ZoomConstantExpression('source', expression.value): SourceExpression));
}
const interpolationType = zoomCurve instanceof Interpolate ? zoomCurve.interpolation : undefined;
return success(isFeatureConstant ?
(new ZoomDependentExpression('camera', expression.value, zoomCurve.labels, interpolationType): CameraExpression) :
(new ZoomDependentExpression('composite', expression.value, zoomCurve.labels, interpolationType): CompositeExpression));
}
import {isFunction, createFunction} from '../function/index.js';
import {Color} from './values.js';
// serialization wrapper for old-style stop functions normalized to the
// expression interface
export class StylePropertyFunction<T> {
_parameters: PropertyValueSpecification<T>;
_specification: StylePropertySpecification;
kind: EvaluationKind;
evaluate: (globals: GlobalProperties, feature?: Feature) => any;
interpolationFactor: ?(input: number, lower: number, upper: number) => number;
zoomStops: ?Array<number>;
constructor(parameters: PropertyValueSpecification<T>, specification: StylePropertySpecification) {
this._parameters = parameters;
this._specification = specification;
extend(this, createFunction(this._parameters, this._specification));
}
static deserialize(serialized: {_parameters: PropertyValueSpecification<T>, _specification: StylePropertySpecification}): StylePropertyFunction<T> {
return new StylePropertyFunction(serialized._parameters, serialized._specification);
}
static serialize(input: StylePropertyFunction<T>): {_parameters: PropertyValueSpecification<T>, _specification: StylePropertySpecification} {
return {
_parameters: input._parameters,
_specification: input._specification
};
}
}
export function normalizePropertyExpression<T>(value: PropertyValueSpecification<T>, specification: StylePropertySpecification): StylePropertyExpression {
if (isFunction(value)) {
return (new StylePropertyFunction(value, specification): any);
} else if (isExpression(value)) {
const expression = createPropertyExpression(value, specification);
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: any = value;
if (typeof value === 'string' && specification.type === 'color') {
constant = Color.parse(value);
}
return {
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: Expression): Step | Interpolate | ParsingError | null {
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 ParsingError) {
return result;
}
expression.eachChild((child) => {
const childResult = findZoomCurve(child);
if (childResult instanceof ParsingError) {
result = childResult;
} else if (!result && childResult) {
result = new ParsingError('', '"zoom" expression may only be used as input to a top-level "step" or "interpolate" expression.');
} else if (result && childResult && result !== childResult) {
result = new ParsingError('', 'Only one zoom-based "step" or "interpolate" subexpression may be used in an expression.');
}
});
return result;
}
import {ColorType, StringType, NumberType, BooleanType, ValueType, FormattedType, ResolvedImageType, array} from './types.js';
function getExpectedType(spec: StylePropertySpecification): Type {
const types = {
color: ColorType,
string: StringType,
number: NumberType,
enum: StringType,
boolean: BooleanType,
formatted: FormattedType,
resolvedImage: ResolvedImageType
};
if (spec.type === 'array') {
return array(types[spec.value] || ValueType, spec.length);
}
return types[spec.type];
}
function getDefaultValue(spec: StylePropertySpecification): Value {
if (spec.type === 'color' && (isFunction(spec.default) || Array.isArray(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);
} else if (spec.type === 'color') {
return Color.parse(spec.default) || null;
} else if (spec.default === undefined) {
return null;
} else {
return spec.default;
}
}

View File

@@ -0,0 +1,59 @@
// @flow
import CompoundExpression from './compound_expression.js';
import Within from './definitions/within.js';
import type {Expression} from './expression.js';
function isFeatureConstant(e: Expression): boolean {
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;
}
let result = true;
e.eachChild(arg => {
if (result && !isFeatureConstant(arg)) { result = false; }
});
return result;
}
function isStateConstant(e: Expression): boolean {
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: Expression, properties: Array<string>): boolean {
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;
}
export {isFeatureConstant, isGlobalPropertyConstant, isStateConstant};

View File

@@ -0,0 +1,233 @@
// @flow
import Scope from './scope.js';
import {checkSubtype} from './types.js';
import ParsingError from './parsing_error.js';
import Literal from './definitions/literal.js';
import Assertion from './definitions/assertion.js';
import Coercion from './definitions/coercion.js';
import EvaluationContext from './evaluation_context.js';
import CompoundExpression from './compound_expression.js';
import CollatorExpression from './definitions/collator.js';
import Within from './definitions/within.js';
import {isGlobalPropertyConstant, isFeatureConstant} from './is_constant.js';
import Var from './definitions/var.js';
import type {Expression, ExpressionRegistry} from './expression.js';
import type {Type} from './types.js';
/**
* State associated parsing at a given point in an expression tree.
* @private
*/
class ParsingContext {
registry: ExpressionRegistry;
path: Array<number>;
key: string;
scope: Scope;
errors: Array<ParsingError>;
// The expected type of this expression. Provided only to allow Expression
// implementations to infer argument types: Expression#parse() need not
// check that the output type of the parsed expression matches
// `expectedType`.
expectedType: ?Type;
constructor(
registry: ExpressionRegistry,
path: Array<number> = [],
expectedType: ?Type,
scope: Scope = new Scope(),
errors: Array<ParsingError> = []
) {
this.registry = registry;
this.path = path;
this.key = path.map(part => `[${part}]`).join('');
this.scope = scope;
this.errors = errors;
this.expectedType = expectedType;
}
/**
* @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: mixed,
index?: number,
expectedType?: ?Type,
bindings?: Array<[string, Expression]>,
options: {typeAnnotation?: 'assert' | 'coerce' | 'omit'} = {}
): ?Expression {
if (index) {
return this.concat(index, expectedType, bindings)._parse(expr, options);
}
return this._parse(expr, options);
}
_parse(expr: mixed, options: {typeAnnotation?: 'assert' | 'coerce' | 'omit'}): ?Expression {
if (expr === null || typeof expr === 'string' || typeof expr === 'boolean' || typeof expr === 'number') {
expr = ['literal', expr];
}
function annotate(parsed, type, typeAnnotation: 'assert' | 'coerce' | 'omit') {
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 ((expected.kind === 'color' || expected.kind === 'formatted' || expected.kind === 'resolvedImage') && (actual.kind === 'value' || actual.kind === 'string')) {
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') && 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: number, expectedType?: ?Type, bindings?: Array<[string, Expression]>): ParsingContext {
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,
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: string, ...keys: Array<number>) {
const key = `${this.key}${keys.map(k => `[${k}]`).join('')}`;
this.errors.push(new ParsingError(key, error));
}
/**
* Returns null if `t` is a subtype of `expected`; otherwise returns an
* error message and also pushes it to `this.errors`.
*/
checkSubtype(expected: Type, t: Type): ?string {
const error = checkSubtype(expected, t);
if (error) this.error(error);
return error;
}
}
export default ParsingContext;
function isConstant(expression: Expression) {
if (expression instanceof Var) {
return isConstant(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;
}
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 && isConstant(child);
} else {
childrenConstant = childrenConstant && child instanceof Literal;
}
});
if (!childrenConstant) {
return false;
}
return isFeatureConstant(expression) &&
isGlobalPropertyConstant(expression, ['zoom', 'heatmap-density', 'line-progress', 'sky-radial-progress', 'accumulated', 'is-supported-script', 'pitch', 'distance-from-center']);
}

View File

@@ -0,0 +1,13 @@
// @flow
class ParsingError extends Error {
key: string;
message: string;
constructor(key: string, message: string) {
super(message);
this.message = message;
this.key = key;
}
}
export default ParsingError;

View File

@@ -0,0 +1,17 @@
// @flow
class RuntimeError {
name: string;
message: string;
constructor(message: string) {
this.name = 'ExpressionEvaluationError';
this.message = message;
}
toJSON(): string {
return this.message;
}
}
export default RuntimeError;

View File

@@ -0,0 +1,36 @@
// @flow
import type {Expression} from './expression.js';
/**
* Tracks `let` bindings during expression parsing.
* @private
*/
class Scope {
parent: ?Scope;
bindings: {[_: string]: Expression};
constructor(parent?: Scope, bindings: Array<[string, Expression]> = []) {
this.parent = parent;
this.bindings = {};
for (const [name, expression] of bindings) {
this.bindings[name] = expression;
}
}
concat(bindings: Array<[string, Expression]>): Scope {
return new Scope(this, bindings);
}
get(name: string): Expression {
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: string): boolean {
if (this.bindings[name]) return true;
return this.parent ? this.parent.has(name) : false;
}
}
export default Scope;

View File

@@ -0,0 +1,39 @@
// @flow
import RuntimeError from './runtime_error.js';
import type {Expression} from './expression.js';
export type Stops = Array<[number, Expression]>;
/**
* Returns the index of the last stop <= input, or 0 if it doesn't exist.
* @private
*/
export function findStopLessThanOrEqualTo(stops: Array<number>, input: number): number {
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;
}

View File

@@ -0,0 +1,126 @@
// @flow
export type NullTypeT = { kind: 'null' };
export type NumberTypeT = { kind: 'number' };
export type StringTypeT = { kind: 'string' };
export type BooleanTypeT = { kind: 'boolean' };
export type ColorTypeT = { kind: 'color' };
export type ObjectTypeT = { kind: 'object' };
export type ValueTypeT = { kind: 'value' };
export type ErrorTypeT = { kind: 'error' };
export type CollatorTypeT = { kind: 'collator' };
export type FormattedTypeT = { kind: 'formatted' };
export type ResolvedImageTypeT = { kind: 'resolvedImage' };
export type EvaluationKind = 'constant' | 'source' | 'camera' | 'composite';
export type Type =
NullTypeT |
NumberTypeT |
StringTypeT |
BooleanTypeT |
ColorTypeT |
ObjectTypeT |
ValueTypeT |
ArrayType | // eslint-disable-line no-use-before-define
ErrorTypeT |
CollatorTypeT |
FormattedTypeT |
ResolvedImageTypeT
export type ArrayType = {
kind: 'array',
itemType: Type,
N: ?number
}
export type NativeType = 'number' | 'string' | 'boolean' | 'null' | 'array' | 'object'
export const NullType = {kind: 'null'};
export const NumberType = {kind: 'number'};
export const StringType = {kind: 'string'};
export const BooleanType = {kind: 'boolean'};
export const ColorType = {kind: 'color'};
export const ObjectType = {kind: 'object'};
export const ValueType = {kind: 'value'};
export const ErrorType = {kind: 'error'};
export const CollatorType = {kind: 'collator'};
export const FormattedType = {kind: 'formatted'};
export const ResolvedImageType = {kind: 'resolvedImage'};
export function array(itemType: Type, N: ?number): ArrayType {
return {
kind: 'array',
itemType,
N
};
}
export function toString(type: Type): string {
if (type.kind === 'array') {
const itemType = toString(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,
FormattedType,
ObjectType,
array(ValueType),
ResolvedImageType
];
/**
* Returns null if `t` is a subtype of `expected`; otherwise returns an
* error message.
* @private
*/
export function checkSubtype(expected: Type, t: Type): ?string {
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 ${toString(expected)} but found ${toString(t)} instead.`;
}
export function isValidType(provided: Type, allowedTypes: Array<Type>): boolean {
return allowedTypes.some(t => t.kind === provided.kind);
}
export function isValidNativeType(provided: any, allowedTypes: Array<NativeType>): boolean {
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;
}
});
}

View File

@@ -0,0 +1,61 @@
// @flow
// Flow type declarations for Intl cribbed from
// https://github.com/facebook/flow/issues/1270
declare var Intl: {
Collator: Class<Intl$Collator>
};
declare class Intl$Collator {
constructor (
locales?: string | string[],
options?: CollatorOptions
): Intl$Collator;
static (
locales?: string | string[],
options?: CollatorOptions
): Intl$Collator;
compare (a: string, b: string): number;
resolvedOptions(): any;
}
type CollatorOptions = {
localeMatcher?: 'lookup' | 'best fit',
usage?: 'sort' | 'search',
sensitivity?: 'base' | 'accent' | 'case' | 'variant',
ignorePunctuation?: boolean,
numeric?: boolean,
caseFirst?: 'upper' | 'lower' | 'false'
}
export default class Collator {
locale: string | null;
sensitivity: 'base' | 'accent' | 'case' | 'variant';
collator: Intl$Collator;
constructor(caseSensitive: boolean, diacriticSensitive: boolean, locale: string | null) {
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: string, rhs: string): number {
return this.collator.compare(lhs, rhs);
}
resolvedLocale(): string {
// 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;
}
}

View File

@@ -0,0 +1,74 @@
// @flow
import type Color from '../../util/color.js';
import type ResolvedImage from '../types/resolved_image.js';
export class FormattedSection {
text: string;
image: ResolvedImage | null;
scale: number | null;
fontStack: string | null;
textColor: Color | null;
constructor(text: string, image: ResolvedImage | null, scale: number | null, fontStack: string | null, textColor: Color | null) {
// combine characters so that diacritic marks are not separate code points
this.text = text.normalize ? text.normalize() : text;
this.image = image;
this.scale = scale;
this.fontStack = fontStack;
this.textColor = textColor;
}
}
export default class Formatted {
sections: Array<FormattedSection>;
constructor(sections: Array<FormattedSection>) {
this.sections = sections;
}
static fromString(unformatted: string): Formatted {
return new Formatted([new FormattedSection(unformatted, null, null, null, null)]);
}
isEmpty(): boolean {
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: Formatted | string): Formatted {
if (text instanceof Formatted) {
return text;
} else {
return Formatted.fromString(text);
}
}
toString(): string {
if (this.sections.length === 0) return '';
return this.sections.map(section => section.text).join('');
}
serialize(): Array<mixed> {
const serialized: Array<mixed> = ["format"];
for (const section of this.sections) {
if (section.image) {
serialized.push(["image", section.image.name]);
continue;
}
serialized.push(section.text);
const options: { [key: string]: mixed } = {};
if (section.fontStack) {
options["text-font"] = ["literal", section.fontStack.split(',')];
}
if (section.scale) {
options["font-scale"] = section.scale;
}
if (section.textColor) {
options["text-color"] = (["rgba"]: Array<mixed>).concat(section.textColor.toArray());
}
serialized.push(options);
}
return serialized;
}
}

View File

@@ -0,0 +1,29 @@
// @flow
export type ResolvedImageOptions = {
name: string,
available: boolean
};
export default class ResolvedImage {
name: string;
available: boolean;
constructor(options: ResolvedImageOptions) {
this.name = options.name;
this.available = options.available;
}
toString(): string {
return this.name;
}
static fromString(name: string): ResolvedImage | null {
if (!name) return null; // treat empty values as no image
return new ResolvedImage({name, available: false});
}
serialize(): Array<string> {
return ["image", this.name];
}
}

View File

@@ -0,0 +1,123 @@
// @flow
import assert from 'assert';
import Color from '../util/color.js';
import Collator from './types/collator.js';
import Formatted from './types/formatted.js';
import ResolvedImage from './types/resolved_image.js';
import {NullType, NumberType, StringType, BooleanType, ColorType, ObjectType, ValueType, CollatorType, FormattedType, ResolvedImageType, array} from './types.js';
import type {Type} from './types.js';
export function validateRGBA(r: mixed, g: mixed, b: mixed, a?: mixed): string | null {
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;
}
export type Value = null | string | boolean | number | Color | Collator | Formatted | ResolvedImage | $ReadOnlyArray<Value> | { +[string]: Value }
export function isValue(mixed: mixed): boolean {
if (mixed === null) {
return true;
} else if (typeof mixed === 'string') {
return true;
} else if (typeof mixed === 'boolean') {
return true;
} else if (typeof mixed === 'number') {
return true;
} else if (mixed instanceof Color) {
return true;
} else if (mixed instanceof Collator) {
return true;
} else if (mixed instanceof Formatted) {
return true;
} else if (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;
}
}
export function typeOf(value: Value): Type {
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 Collator) {
return CollatorType;
} else if (value instanceof Formatted) {
return FormattedType;
} else if (value instanceof ResolvedImage) {
return ResolvedImageType;
} else if (Array.isArray(value)) {
const length = value.length;
let itemType: Type | typeof undefined;
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 {
assert(typeof value === 'object');
return ObjectType;
}
}
export function toString(value: Value): string {
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 Formatted || value instanceof ResolvedImage) {
return value.toString();
} else {
return JSON.stringify(value);
}
}
export {Color, Collator};

View File

@@ -0,0 +1,55 @@
## Filter
Filter expressions are used to target specific data in a layer. This library implements the semantics specified by the [Mapbox GL JS spec](https://www.mapbox.com/mapbox-gl-style-spec/#filter).
### API
`featureFilter(filter)`
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.
#### Parameters
| parameter | type | description |
| --------- | ----- | ---------------- |
| `filter` | Array | mapbox gl filter |
**Returns** `Function`, filter-evaluating function
### Usage
``` javascript
var ff = require('@mapbox/mapbox-gl-style-spec').featureFilter;
// will match a feature with class of street_limited,
// AND an admin_level less than or equal to 3,
// that's NOT a polygon.
var filter = [
"all",
["==", "class", "street_limited"],
["<=", "admin_level", 3],
["!=", "$type", "Polygon"]
]
// will match a feature that has a class of
// wetland OR wetland_noveg.
// ["in", "class", "wetland", "wetland_noveg"]
// testFilter will be a function that returns a boolean
var testFilter = ff(filter);
// Layer feature that you're testing. Must have type
// and properties keys.
var feature = {
type: 2,
properties: {
class: "street_limited",
admin_level: 1
}
};
// will return a boolean based on whether the feature matched the filter
return testFilter.filter({zoom: 0}, feature);
```

View File

@@ -0,0 +1,208 @@
// @flow
import {isExpressionFilter} from './index.js';
import type {FilterSpecification} from '../types.js';
type ExpectedTypes = {[_: string]: 'string' | 'number' | 'boolean'};
/**
* Convert the given legacy filter to (the JSON representation of) an
* equivalent expression
* @private
*/
export default function convertFilter(filter: FilterSpecification): mixed {
return _convertFilter(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 evalaution 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: FilterSpecification, expectedTypes: ExpectedTypes): mixed {
if (isExpressionFilter(filter)) { return filter; }
if (!filter) return true;
const op = filter[0];
if (filter.length <= 1) return (op !== 'any');
let converted;
if (
op === '==' ||
op === '!=' ||
op === '<' ||
op === '>' ||
op === '<=' ||
op === '>='
) {
const [, property, value] = (filter: any);
converted = convertComparisonOp(property, value, op, expectedTypes);
} else if (op === 'any') {
const children = (filter: any).slice(1).map(f => {
const types = {};
const child = _convertFilter(f, types);
const typechecks = runtimeTypeChecks(types);
return typechecks === true ? child : ['case', typechecks, child, false];
});
return ['any'].concat(children);
} else if (op === 'all') {
const children = (filter: any).slice(1).map(f => _convertFilter(f, expectedTypes));
return children.length > 1 ? ['all'].concat(children) : [].concat(...children);
} else if (op === 'none') {
return ['!', _convertFilter(['any'].concat((filter: any).slice(1)), {})];
} else if (op === 'in') {
converted = convertInOp((filter[1]: any), filter.slice(2));
} else if (op === '!in') {
converted = convertInOp((filter[1]: any), filter.slice(2), true);
} else if (op === 'has') {
converted = convertHasOp((filter[1]: any));
} else if (op === '!has') {
converted = ['!', convertHasOp((filter[1]: any))];
} else {
converted = true;
}
return converted;
}
// 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: 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'].concat(conditions);
}
function convertComparisonOp(property: string, value: any, op: string, expectedTypes: ?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): any);
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: string, values: Array<any>, 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];
}
return [ negate ? 'all' : 'any' ].concat(
values.map(v => [negate ? '!=' : '==', get, v])
);
}
function convertHasOp(property: string) {
if (property === '$type') {
return true;
} else if (property === '$id') {
return ['!=', ['id'], null];
} else {
return ['has', property];
}
}

View File

@@ -0,0 +1,336 @@
// @flow
import {createExpression} from '../expression/index.js';
import {isFeatureConstant} from '../expression/is_constant.js';
import {deepUnbundle} from '../util/unbundle_jsonlint.js';
import latest from '../reference/latest.js';
import type {GlobalProperties, Feature} from '../expression/index.js';
import type {CanonicalTileID} from '../../source/tile_id.js';
import type Point from '@mapbox/point-geometry';
export type FeatureDistanceData = {bearing: [number, number], center: [number, number], scale: number};
export type FilterExpression = (globalProperties: GlobalProperties, feature: Feature, canonical?: CanonicalTileID, featureTileCoord?: Point, featureDistanceData?: FeatureDistanceData) => boolean;
export type FeatureFilter = {filter: FilterExpression, dynamicFilter?: FilterExpression, needGeometry: boolean, needFeature: boolean};
export default createFilter;
export {isExpressionFilter, isDynamicFilter, extractStaticFilter};
function isExpressionFilter(filter: any): boolean {
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;
}
}
/**
* 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 {Array} filter mapbox gl filter
* @param {string} layerType the type of the layer this filter will be applied to.
* @returns {Function} filter-evaluating function
*/
function createFilter(filter: any, layerType?: string = 'fill'): FeatureFilter {
if (filter === null || filter === undefined) {
return {filter: () => true, needGeometry: false, needFeature: false};
}
if (!isExpressionFilter(filter)) {
filter = convertFilter(filter);
}
const filterExp = ((filter: any): string[] | string | boolean);
let staticFilter = true;
try {
staticFilter = extractStaticFilter(filterExp);
} catch (e) {
console.warn(
`Failed to extract static filter. Filter will continue working, but at higher memory usage and slower framerate.
This is most likely a bug, please report this via https://github.com/mapbox/mapbox-gl-js/issues/new?assignees=&labels=&template=Bug_report.md
and paste the contents of this message in the report.
Thank you!
Filter Expression:
${JSON.stringify(filterExp, null, 2)}
`);
}
// Compile the static component of the filter
const filterSpec = latest[`filter_${layerType}`];
const compiledStaticFilter = createExpression(staticFilter, filterSpec);
let filterFunc = null;
if (compiledStaticFilter.result === 'error') {
throw new Error(compiledStaticFilter.value.map(err => `${err.key}: ${err.message}`).join(', '));
} else {
filterFunc = (globalProperties: GlobalProperties, feature: Feature, canonical?: CanonicalTileID) => compiledStaticFilter.value.evaluate(globalProperties, feature, {}, canonical);
}
// If the static component is not equal to the entire filter then we have a dynamic component
// Compile the dynamic component separately
let dynamicFilterFunc = null;
let needFeature = null;
if (staticFilter !== filterExp) {
const compiledDynamicFilter = createExpression(filterExp, filterSpec);
if (compiledDynamicFilter.result === 'error') {
throw new Error(compiledDynamicFilter.value.map(err => `${err.key}: ${err.message}`).join(', '));
} else {
dynamicFilterFunc = (globalProperties: GlobalProperties, feature: Feature, canonical?: CanonicalTileID, featureTileCoord?: Point, featureDistanceData?: FeatureDistanceData) => compiledDynamicFilter.value.evaluate(globalProperties, feature, {}, canonical, undefined, undefined, featureTileCoord, featureDistanceData);
needFeature = !isFeatureConstant(compiledDynamicFilter.value.expression);
}
}
filterFunc = ((filterFunc: any): FilterExpression);
const needGeometry = geometryNeeded(staticFilter);
return {
filter: filterFunc,
dynamicFilter: dynamicFilterFunc ? dynamicFilterFunc : undefined,
needGeometry,
needFeature: !!needFeature
};
}
function extractStaticFilter(filter: any): any {
if (!isDynamicFilter(filter)) {
return filter;
}
// Shallow copy so we can replace expressions in-place
let result = deepUnbundle(filter);
// 1. Union branches
unionDynamicBranches(result);
// 2. Collapse dynamic conditions to `true`
result = collapseDynamicBooleanExpressions(result);
return result;
}
function collapseDynamicBooleanExpressions(expression: any): any {
if (!Array.isArray(expression)) {
return expression;
}
const collapsed = collapsedExpression(expression);
if (collapsed === true) {
return collapsed;
} else {
return collapsed.map((subExpression) => collapseDynamicBooleanExpressions(subExpression));
}
}
/**
* Traverses the expression and replaces all instances of branching on a
* `dynamic` conditional (such as `['pitch']` or `['distance-from-center']`)
* into an `any` expression.
* This ensures that all possible outcomes of a `dynamic` branch are considered
* when evaluating the expression upfront during filtering.
*
* @param {Array<any>} filter the filter expression mutated in-place.
*/
function unionDynamicBranches(filter: any) {
let isBranchingDynamically = false;
const branches = [];
if (filter[0] === 'case') {
for (let i = 1; i < filter.length - 1; i += 2) {
isBranchingDynamically = isBranchingDynamically || isDynamicFilter(filter[i]);
branches.push(filter[i + 1]);
}
branches.push(filter[filter.length - 1]);
} else if (filter[0] === 'match') {
isBranchingDynamically = isBranchingDynamically || isDynamicFilter(filter[1]);
for (let i = 2; i < filter.length - 1; i += 2) {
branches.push(filter[i + 1]);
}
branches.push(filter[filter.length - 1]);
} else if (filter[0] === 'step') {
isBranchingDynamically = isBranchingDynamically || isDynamicFilter(filter[1]);
for (let i = 1; i < filter.length - 1; i += 2) {
branches.push(filter[i + 1]);
}
}
if (isBranchingDynamically) {
filter.length = 0;
filter.push('any', ...branches);
}
// traverse and recurse into children
for (let i = 1; i < filter.length; i++) {
unionDynamicBranches(filter[i]);
}
}
function isDynamicFilter(filter: any): boolean {
// Base Cases
if (!Array.isArray(filter)) {
return false;
}
if (isRootExpressionDynamic(filter[0])) {
return true;
}
for (let i = 1; i < filter.length; i++) {
const child = filter[i];
if (isDynamicFilter(child)) {
return true;
}
}
return false;
}
function isRootExpressionDynamic(expression: string): boolean {
return expression === 'pitch' ||
expression === 'distance-from-center';
}
const dynamicConditionExpressions = new Set([
'in',
'==',
'!=',
'>',
'>=',
'<',
'<=',
'to-boolean'
]);
function collapsedExpression(expression: any): any {
if (dynamicConditionExpressions.has(expression[0])) {
for (let i = 1; i < expression.length; i++) {
const param = expression[i];
if (isDynamicFilter(param)) {
return true;
}
}
}
return 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') return true;
for (let index = 1; index < filter.length; index++) {
if (geometryNeeded(filter[index])) return true;
}
return false;
}
function convertFilter(filter: ?Array<any>): mixed {
if (!filter) return true;
const op = filter[0];
if (filter.length <= 1) return (op !== 'any');
const converted =
op === '==' ? convertComparisonOp(filter[1], filter[2], '==') :
op === '!=' ? convertNegation(convertComparisonOp(filter[1], filter[2], '==')) :
op === '<' ||
op === '>' ||
op === '<=' ||
op === '>=' ? convertComparisonOp(filter[1], filter[2], op) :
op === 'any' ? convertDisjunctionOp(filter.slice(1)) :
op === 'all' ? ['all'].concat(filter.slice(1).map(convertFilter)) :
op === 'none' ? ['all'].concat(filter.slice(1).map(convertFilter).map(convertNegation)) :
op === 'in' ? convertInOp(filter[1], filter.slice(2)) :
op === '!in' ? convertNegation(convertInOp(filter[1], filter.slice(2))) :
op === 'has' ? convertHasOp(filter[1]) :
op === '!has' ? convertNegation(convertHasOp(filter[1])) :
op === 'within' ? filter :
true;
return converted;
}
function convertComparisonOp(property: string, value: any, op: string) {
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: Array<Array<any>>) {
return ['any'].concat(filters.map(convertFilter));
}
function convertInOp(property: string, values: Array<any>) {
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(property: string) {
switch (property) {
case '$type':
return true;
case '$id':
return [`filter-has-id`];
default:
return [`filter-has`, property];
}
}
function convertNegation(filter: mixed) {
return ['!', filter];
}

View File

@@ -0,0 +1,43 @@
// @flow strict
type GeoJSONPosition = [number, number] | [number, number, number];
type Geometry<T, C> = { type: T, coordinates: C }
declare module "@mapbox/geojson-types" {
declare export type GeoJSONPoint = Geometry<'Point', GeoJSONPosition>;
declare export type GeoJSONMultiPoint = Geometry<'MultiPoint', Array<GeoJSONPosition>>;
declare export type GeoJSONLineString = Geometry<'LineString', Array<GeoJSONPosition>>;
declare export type GeoJSONMultiLineString = Geometry<'MultiLineString', Array<Array<GeoJSONPosition>>>;
declare export type GeoJSONPolygon = Geometry<'Polygon', Array<Array<GeoJSONPosition>>>;
declare export type GeoJSONMultiPolygon = Geometry<'MultiPolygon', Array<Array<Array<GeoJSONPosition>>>>;
declare export type GeoJSONGeometry =
| GeoJSONPoint
| GeoJSONMultiPoint
| GeoJSONLineString
| GeoJSONMultiLineString
| GeoJSONPolygon
| GeoJSONMultiPolygon
| GeoJSONGeometryCollection;
declare export type GeoJSONGeometryCollection = {
type: 'GeometryCollection',
geometries: Array<GeoJSONGeometry>
};
declare export type GeoJSONFeature = {
type: 'Feature',
geometry: ?GeoJSONGeometry,
properties: ?{},
id?: number | string
};
declare export type GeoJSONFeatureCollection = {
type: 'FeatureCollection',
features: Array<GeoJSONFeature>
};
declare export type GeoJSON = GeoJSONGeometry | GeoJSONFeature | GeoJSONFeatureCollection;
}

View File

@@ -0,0 +1,113 @@
// @flow
type VecType = Array<number> | Float32Array | Float64Array;
declare module "gl-matrix" {
declare type Vec2 = VecType;
declare type Vec3 = VecType;
declare type Vec4 = VecType;
declare type Quat = VecType;
declare type Mat2 = VecType;
declare type Mat3 = VecType;
declare type Mat4 = VecType;
declare var vec2: {
exactEquals(Vec2, Vec2): boolean
};
declare var vec3: {
create(): Float32Array,
fromValues(number, number, number): Float32Array,
length(Vec3): number,
len(Vec3): number,
distance(Vec3, Vec3): number,
squaredLength(Vec3): number,
dot(Vec3, Vec3): number,
equals(Vec3, Vec3): boolean,
exactEquals(Vec3, Vec3): boolean,
clone<T: Vec3>(T): T,
normalize<T: Vec3>(T, Vec3): T,
add<T: Vec3>(T, Vec3, Vec3): T,
sub<T: Vec3>(T, Vec3, Vec3): T,
set<T: Vec3>(T, number, number, number): T,
subtract<T: Vec3>(T, Vec3, Vec3): T,
cross<T: Vec3>(T, Vec3, Vec3): T,
negate<T: Vec3>(T, Vec3): T,
scale<T: Vec3>(T, Vec3, number): T,
scaleAndAdd<T: Vec3>(T, Vec3, Vec3, number): T,
multiply<T: Vec3>(T, Vec3, Vec3): T,
mul<T: Vec3>(T, Vec3, Vec3): T,
div<T: Vec3>(T, Vec3, Vec3): T,
min<T: Vec3>(T, Vec3, Vec3): T,
max<T: Vec3>(T, Vec3, Vec3): T,
lerp<T: Vec3>(T, Vec3, Vec3, number): T,
transformQuat<T: Vec3>(T, Vec3, Quat): T,
transformMat3<T: Vec3>(T, Vec3, Mat3): T,
transformMat4<T: Vec3>(T, Vec3, Mat4): T,
angle(Vec3, Vec3): number;
rotateX<T: Vec3>(T, Vec3, Vec3, number): T,
rotateY<T: Vec3>(T, Vec3, Vec3, number): T,
rotateZ<T: Vec3>(T, Vec3, Vec3, number): T,
};
declare var vec4: {
scale<T: Vec4>(T, Vec4, number): T,
mul<T: Vec4>(T, Vec4, Vec4): T,
transformMat4<T: Vec4>(T, Vec4, Mat4): T,
normalize<T: Vec4>(T, Vec4): T
};
declare var mat2: {
create(): Float32Array,
rotate<T: Mat2>(T, Mat2, number): T,
invert<T: Mat2>(T, Mat2): T,
scale<T: Mat2>(T, Mat2, Vec2): T
};
declare var mat3: {
create(): Float32Array,
fromMat4<T: Mat3>(T, Mat4): T,
fromRotation<T: Mat3>(T, number): T,
mul<T: Mat3>(T, Mat3, Mat3): T,
multiply<T: Mat3>(T, Mat3, Mat3): T,
adjoint<T: Mat3>(T, Mat3): T,
transpose<T: Mat3>(T, Mat3): T
};
declare var mat4: {
create(): Float32Array,
fromScaling<T: Mat4>(T, Vec3): T,
fromTranslation<T: Mat4>(T, Vec3): T,
fromQuat<T: Mat4>(T, Quat): T,
ortho<T: Mat4>(T, number, number, number, number, number, number): T,
perspective<T: Mat4>(T, number, number, number, number): T,
identity<T: Mat4>(T): T,
scale<T: Mat4>(T, Mat4, Vec3): T,
mul<T: Mat4>(T, Mat4, Mat4): T,
multiply<T: Mat4>(T, Mat4, Mat4): T,
rotateX<T: Mat4>(T, Mat4, number): T,
rotateY<T: Mat4>(T, Mat4, number): T,
rotateZ<T: Mat4>(T, Mat4, number): T,
rotate<T: Mat4>(T, Mat4, number, Vec3): T,
fromRotation<T: Mat4>(T, number, Vec3): T,
translate<T: Mat4>(T, Mat4, Vec3): T,
invert<T: Mat4>(T, Mat4): T,
copy<T: Mat4>(T, Mat4): T,
clone<T: Mat4>(T): T
};
declare var quat: {
create(): Float32Array,
length(Quat): number,
exactEquals(Quat, Quat): boolean,
normalize<T: Quat>(T, Quat): T,
conjugate<T: Quat>(T, Quat): T,
identity<T: Quat>(T): T,
rotateX<T: Quat>(T, Quat, number): T,
rotateY<T: Quat>(T, Quat, number): T,
rotateZ<T: Quat>(T, Quat, number): T
}
}

View File

@@ -0,0 +1,5 @@
// @flow strict
declare module "gl" {
declare function gl(width: number, height: number, attributes: WebGLContextAttributes): WebGLRenderingContext;
declare module.exports: typeof gl;
}

View File

@@ -0,0 +1,13 @@
// @flow strict
declare module 'grid-index' {
declare class GridIndex {
constructor(extent: number, n: number, padding: number): GridIndex;
constructor(data: ArrayBuffer): GridIndex;
insert(key: number, x1: number, y1: number, x2: number, y2: number): void;
query(x1: number, y1: number, x2: number, y2: number, intersectionText?: (number, number, number, number) => boolean): Array<number>;
toArrayBuffer(): ArrayBuffer;
}
declare export default Class<GridIndex>;
}

View File

@@ -0,0 +1,16 @@
// @flow strict
declare module "jsdom" {
declare class JSDOM {
constructor(content: string, options: Object): JSDOM;
window: WindowProxy;
}
declare class VirtualConsole {
constructor(): VirtualConsole;
sendTo(console: typeof console): VirtualConsole;
}
declare module.exports: {
JSDOM: typeof JSDOM,
VirtualConsole: typeof VirtualConsole
};
}

View File

@@ -0,0 +1,16 @@
// @flow
'use strict';
declare module "@mapbox/mapbox-gl-supported" {
declare type SupportedOptions = {failIfMajorPerformanceCaveat: boolean};
declare type SupportedFn = {
(options?: SupportedOptions): boolean,
webGLContextAttributes: WebGLContextAttributes
};
declare function notSupportedReason(options?: SupportedOptions): ?string;
declare module.exports: {
supported: SupportedFn;
notSupportedReason: typeof notSupportedReason;
}
}

View File

@@ -0,0 +1,14 @@
'use strict';
// @flow
declare module "@mapbox/unitbezier" {
declare class UnitBezier {
constructor(p1x: number, p1y: number, p2x: number, p2y: number): UnitBezier;
sampleCurveX(t: number): number;
sampleCurveY(t: number): number;
sampleCurveDerivativeX(t: number): number;
solveCurveX(x: number, epsilon: number | void): number;
solve(x: number, epsilon: number | void): number;
}
declare module.exports: typeof UnitBezier;
}

View File

@@ -0,0 +1,9 @@
// @flow strict
declare class OffscreenCanvas {
width: number;
height: number;
constructor(width: number, height: number): OffscreenCanvas;
getContext(contextType: '2d'): CanvasRenderingContext2D;
}

View File

@@ -0,0 +1,26 @@
// @flow
declare module "pbf" {
declare type ReadFunction<T> = (tag: number, result: T, pbf: Pbf) => void;
declare class Pbf {
constructor(buf?: ArrayBuffer | Uint8Array): Pbf;
readFields<T>(readField: ReadFunction<T>, result: T, end?: number): T;
readMessage<T>(readField: ReadFunction<T>, result: T): T;
readFixed32(): number;
readSFixed32(): number;
readFixed64(): number;
readSFixed64(): number;
readFloat(): number;
readDouble(): number;
readVarint(): number;
readVarint64(): number;
readSVarint(): number;
readBoolean(): boolean;
readString(): string;
readBytes(): Uint8Array;
}
declare export default Class<Pbf>;
}

View File

@@ -0,0 +1,46 @@
// @flow strict
declare module "@mapbox/point-geometry" {
declare export type PointLike = Point | [number, number];
declare class Point {
x: number;
y: number;
constructor(x: number, y: number): Point;
clone(): Point;
add(point: Point): Point;
sub(point: Point): Point;
multByPoint(point: Point): Point;
divByPoint(point: Point): Point;
mult(k: number): Point;
div(k: number): Point;
rotate(angle: number): Point;
rotateAround(angle: number, point: Point): Point;
matMult(matrix: [number, number, number, number]): Point;
unit(): Point;
perp(): Point;
round(): Point;
mag(): number;
equals(point: Point): boolean;
dist(point: Point): number;
distSqr(point: Point): number;
angle(): number;
angleTo(point: Point): number;
angleWith(point: Point): number;
angleWithSep(x: number, y: number): number;
_matMult(matrix: [number, number, number, number]): Point;
_add(point: Point): Point;
_sub(point: Point): Point;
_mult(k: number): Point;
_div(k: number): Point;
_multByPoint(point: Point): Point;
_divByPoint(point: Point): Point;
_unit(): Point;
_perp(): Point;
_rotate(angle: number): Point;
_rotateAround(angle: number, point: Point): Point;
_round(): Point;
static convert(a: PointLike): Point;
}
declare export default Class<Point>;
}

View File

@@ -0,0 +1,13 @@
// @flow
declare module "potpack" {
declare type Bin = {
x: number,
y: number,
w: number,
h: number
};
declare function potpack(bins: Array<Bin>): {w: number, h: number, fill: number};
declare module.exports: typeof potpack;
}

View File

@@ -0,0 +1,28 @@
// @flow strict
declare module "sinon" {
declare type SpyCall = {
args: Array<mixed>
};
declare type Spy = {
(): any,
calledOnce: number,
getCall(i: number): SpyCall
};
declare type Stub = {
callsFake(fn: mixed): Spy
};
declare class FakeServer {
xhr: XMLHttpRequest
}
declare type Sandbox = {
xhr: {supportsCORS: boolean},
fakeServer: {create: () => FakeServer},
createSandbox(options: mixed): Sandbox,
stub(obj?: mixed, prop?: string): Stub,
spy(obj?: mixed, prop?: string): Spy,
restore(): void;
};
declare module.exports: Sandbox;
}

View File

@@ -0,0 +1,31 @@
declare module '@mapbox/tiny-sdf' {
declare type TinySDFOptions = {
fontSize?: number;
buffer?: number;
radius?: number;
cutoff?: number;
fontFamily?: string;
fontWeight?: string;
fontStyle?: string;
};
declare type TinySDFGlyph = {
data: Uint8ClampedArray;
width: number;
height: number;
glyphWidth: number;
glyphHeight: number;
glyphTop: number;
glyphLeft: number;
glyphAdvance: number;
};
declare class TinySDF {
fontWeight: string;
constructor(options: TinySDFOptions): TinySDF;
draw(char: string): TinySDFGlyph;
}
declare export default Class<TinySDF>;
}

View File

@@ -0,0 +1,49 @@
// @flow
declare module "@mapbox/vector-tile" {
import type Pbf from 'pbf';
import type Point from '@mapbox/point-geometry';
import type { GeoJSONFeature } from '@mapbox/geojson-types';
declare export interface IVectorTile {
layers: {[_: string]: IVectorTileLayer};
}
declare export interface IVectorTileLayer {
version?: ?number;
name: string;
extent: number;
length: number;
feature(i: number): IVectorTileFeature;
}
declare export interface IVectorTileFeature {
extent: number;
type: 1 | 2 | 3;
id: number;
properties: {[_: string]: string | number | boolean};
loadGeometry(): Array<Array<Point>>;
toGeoJSON(x: number, y: number, z: number): GeoJSONFeature;
}
declare export class VectorTile implements IVectorTile {
constructor(pbf: Pbf): VectorTile;
layers: {[_: string]: IVectorTileLayer};
}
declare export class VectorTileLayer implements IVectorTileLayer {
version?: ?number;
name: string;
extent: number;
length: number;
feature(i: number): IVectorTileFeature;
}
declare export class VectorTileFeature implements IVectorTileFeature {
extent: number;
type: 1 | 2 | 3;
id: number;
properties: {[_: string]: string | number | boolean};
loadGeometry(): Array<Array<Point>>;
toGeoJSON(x: number, y: number, z: number): GeoJSONFeature;
static types: ['Unknown', 'Point', 'LineString', 'Polygon'];
}
}

51
node_modules/@mapbox/mapbox-gl-style-spec/format.js generated vendored Normal file
View File

@@ -0,0 +1,51 @@
import reference from './reference/latest.js';
import stringifyPretty from 'json-stringify-pretty-compact';
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 Mapbox GL 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 Mapbox GL Style
* @param {number} [space] space argument to pass to `JSON.stringify`
* @returns {string} stringified formatted JSON
* @example
* var fs = require('fs');
* var format = require('mapbox-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, reference.$root);
if (style.layers) {
style.layers = style.layers.map((layer) => sortKeysBy(layer, reference.layer));
}
return stringifyPretty(style, {indent: space});
}
export default format;

View File

@@ -0,0 +1,270 @@
// @flow
import assert from 'assert';
import type {StylePropertySpecification} from '../style-spec.js';
import type {ExpressionSpecification} from '../types.js';
function convertLiteral(value) {
return typeof value === 'object' ? ['literal', value] : value;
}
export default function convertFunction(parameters: any, propertySpec: StylePropertySpecification): ExpressionSpecification {
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): Array<mixed> {
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') {
assert(parameters.stops.length > 0 && parameters.stops.length <= 2);
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 {
assert(propertySpec.expression);
return (propertySpec.expression: any).interpolated ? 'exponential' : 'interval';
}
}
// "String with {name} token" => ["concat", "String with ", ["get", "name"], " token"]
export function convertTokenString(s: string): string | ExpressionSpecification {
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;
}

View File

@@ -0,0 +1,262 @@
import * as colorSpaces from '../util/color_spaces.js';
import Color from '../util/color.js';
import extend from '../util/extend.js';
import getType from '../util/get_type.js';
import * as interpolate from '../util/interpolate.js';
import Interpolate from '../expression/definitions/interpolate.js';
import Formatted from '../expression/types/formatted.js';
import ResolvedImage from '../expression/types/resolved_image.js';
import {supportsInterpolation} from '../util/properties.js';
import {findStopLessThanOrEqualTo} from '../expression/stops.js';
export function isFunction(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function identityFunction(x) {
return x;
}
export function createFunction(parameters, propertySpec) {
const isColor = propertySpec.type === 'color';
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');
if (isColor) {
parameters = extend({}, parameters);
if (parameters.stops) {
parameters.stops = parameters.stops.map((stop) => {
return [stop[0], Color.parse(stop[1])];
});
}
if (parameters.default) {
parameters.default = Color.parse(parameters.default);
} else {
parameters.default = Color.parse(propertySpec.default);
}
}
if (parameters.colorSpace && parameters.colorSpace !== 'rgb' && !colorSpaces[parameters.colorSpace]) { // eslint-disable-line import/namespace
throw new Error(`Unknown color space: ${parameters.colorSpace}`);
}
let innerFun;
let hashedStops;
let categoricalKeyType;
if (type === 'exponential') {
innerFun = evaluateExponentialFunction;
} else if (type === 'interval') {
innerFun = evaluateIntervalFunction;
} else if (type === 'categorical') {
innerFun = evaluateCategoricalFunction;
// 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];
} else if (type === 'identity') {
innerFun = evaluateIdentityFunction;
} else {
throw new Error(`Unknown function type "${type}"`);
}
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(parameters.default, propertySpec.default);
}
return innerFun(parameters, propertySpec, value, hashedStops, categoricalKeyType);
}
};
}
}
function coalesce(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(evaluated, parameters.default, propertySpec.default);
}
function evaluateIntervalFunction(parameters, propertySpec, input) {
// Edge cases
if (getType(input) !== 'number') return coalesce(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(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];
let interp = interpolate[propertySpec.type] || identityFunction; // eslint-disable-line import/namespace
if (parameters.colorSpace && parameters.colorSpace !== 'rgb') {
const colorspace = colorSpaces[parameters.colorSpace]; // eslint-disable-line import/namespace
interp = (a, b) => colorspace.reverse(colorspace.interpolate(colorspace.forward(a), colorspace.forward(b), t));
}
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);
}
};
}
return interp(outputLower, outputUpper, t);
}
function evaluateIdentityFunction(parameters, propertySpec, input) {
if (propertySpec.type === 'color') {
input = Color.parse(input);
} else if (propertySpec.type === 'formatted') {
input = Formatted.fromString(input.toString());
} else if (propertySpec.type === 'resolvedImage') {
input = ResolvedImage.fromString(input.toString());
} else if (getType(input) !== propertySpec.type && (propertySpec.type !== 'enum' || !propertySpec.values[input])) {
input = undefined;
}
return coalesce(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);
}
}

View File

@@ -0,0 +1,73 @@
// @flow
import type {LayerSpecification} from './types.js';
import refProperties from './util/ref_properties.js';
function stringify(obj) {
if (typeof obj === 'number' || typeof obj === 'boolean' || typeof obj === 'string' || obj === undefined || obj === null)
return JSON.stringify(obj);
if (Array.isArray(obj)) {
let str = '[';
for (const val of obj) {
str += `${stringify(val)},`;
}
return `${str}]`;
}
let str = '{';
for (const key of Object.keys(obj).sort()) {
str += `${key}:${stringify((obj: any)[key])},`;
}
return `${str}}`;
}
function getKey(layer) {
let key = '';
for (const k of refProperties) {
key += `/${stringify((layer: any)[k])}`;
}
return key;
}
/**
* Given an array of layers, return an array of arrays of layers where all
* layers in each group have identical 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.
*
* @private
* @param {Array<Layer>} layers
* @param {Object} [cachedKeys] - an object to keep already calculated keys.
* @returns {Array<Array<Layer>>}
*/
export default function groupByLayout(layers: Array<LayerSpecification>, cachedKeys: {[id: string]: string}): Array<Array<LayerSpecification>> {
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;
}

36
node_modules/@mapbox/mapbox-gl-style-spec/migrate.js generated vendored Normal file
View File

@@ -0,0 +1,36 @@
import migrateToV8 from './migrate/v8.js';
import migrateToExpressions from './migrate/expressions.js';
/**
* Migrate a Mapbox GL Style to the latest version.
*
* @private
* @alias migrate
* @param {object} style a Mapbox GL Style
* @returns {Object} a migrated style
* @example
* var fs = require('fs');
* var migrate = require('mapbox-gl-style-spec').migrate;
* var style = fs.readFileSync('./style.json', 'utf8');
* fs.writeFileSync('./style.json', JSON.stringify(migrate(style)));
*/
export default function(style) {
let migrated = false;
if (style.version === 7) {
style = migrateToV8(style);
migrated = true;
}
if (style.version === 8) {
migrated = migrateToExpressions(style);
migrated = true;
}
if (!migrated) {
throw new Error('cannot migrate from', style.version);
}
return style;
}

View File

@@ -0,0 +1,39 @@
// @flow
import {
eachLayer,
eachProperty
} from '../visit.js';
import {isExpression} from '../expression/index.js';
import convertFunction, {convertTokenString} from '../function/convert.js';
import convertFilter from '../feature_filter/convert.js';
import type {StyleSpecification} from '../types.js';
/**
* 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.
*/
export default function(style: StyleSpecification): StyleSpecification {
const converted = [];
eachLayer(style, (layer) => {
if (layer.filter) {
layer.filter = (convertFilter(layer.filter): any);
}
});
eachProperty(style, {paint: true, layout: true}, ({path, value, reference, set}) => {
if (isExpression(value)) 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;
}

203
node_modules/@mapbox/mapbox-gl-style-spec/migrate/v8.js generated vendored Normal file
View File

@@ -0,0 +1,203 @@
import URL from 'url';
import {eachSource, eachLayer, eachProperty} from '../visit.js';
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];
}
export default function(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 migrateFontstackURL(input) {
const inputParsed = URL.parse(input);
const inputPathnameParts = inputParsed.pathname.split('/');
if (inputParsed.protocol !== 'mapbox:') {
return input;
} else if (inputParsed.hostname === 'fontstack') {
assert(decodeURI(inputParsed.pathname) === '/{fontstack}/{range}.pbf');
return 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf';
} else if (inputParsed.hostname === 'fonts') {
assert(inputPathnameParts[1] === 'v1');
assert(decodeURI(inputPathnameParts[3]) === '{fontstack}');
assert(decodeURI(inputPathnameParts[4]) === '{range}.pbf');
return `mapbox://fonts/${inputPathnameParts[2]}/{fontstack}/{range}.pbf`;
} else {
assert(false);
}
function assert(predicate) {
if (!predicate) {
throw new Error(`Invalid font url: "${input}"`);
}
}
}
if (style.glyphs) {
style.glyphs = migrateFontstackURL(style.glyphs);
}
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;
}

View File

@@ -0,0 +1,26 @@
import deref from '../deref.js';
function eachLayer(style, callback) {
for (const k in style.layers) {
callback(style.layers[k]);
}
}
export default function(style) {
style.version = 9;
// remove user-specified refs
style.layers = deref(style.layers);
// remove class-specific paint properties
eachLayer(style, (layer) => {
for (const k in layer) {
if (/paint\..*/.test(k)) {
delete layer[k];
}
}
});
return style;
}

53
node_modules/@mapbox/mapbox-gl-style-spec/package.json generated vendored Normal file
View File

@@ -0,0 +1,53 @@
{
"name": "@mapbox/mapbox-gl-style-spec",
"description": "a specification for mapbox gl styles",
"version": "13.28.0",
"author": "Mapbox",
"keywords": [
"mapbox",
"mapbox-gl",
"mapbox-gl-js"
],
"license": "ISC",
"main": "./dist/index.cjs",
"module": "./dist/index.es.js",
"type": "module",
"exports": {
".": {
"require": "./dist/index.cjs",
"import": "./dist/index.es.js"
},
"./": {
"import": "./"
}
},
"scripts": {
"pretest": "npm run build",
"test": "node ./test.js",
"copy-flow-typed": "cp -R ../../flow-typed .",
"build": "../../node_modules/.bin/rollup -c && ../../node_modules/.bin/rollup -c --environment esm",
"prepublishOnly": "npm run copy-flow-typed && npm run build",
"postpublish": "rm -r flow-typed dist/index.cjs"
},
"repository": {
"type": "git",
"url": "git@github.com:mapbox/mapbox-gl-js.git"
},
"bin": {
"gl-style-migrate": "./bin/gl-style-migrate.js",
"gl-style-validate": "./bin/gl-style-validate.js",
"gl-style-format": "./bin/gl-style-format.js",
"gl-style-composite": "./bin/gl-style-composite.js"
},
"dependencies": {
"@mapbox/jsonlint-lines-primitives": "~2.0.2",
"@mapbox/point-geometry": "^0.1.0",
"@mapbox/unitbezier": "^0.0.0",
"csscolorparser": "~1.0.2",
"json-stringify-pretty-compact": "^2.0.0",
"minimist": "^1.2.6",
"rw": "^1.3.3",
"sort-object": "^0.3.2"
},
"sideEffects": false
}

View File

@@ -0,0 +1,14 @@
import ParsingError from './error/parsing_error.js';
import jsonlint from '@mapbox/jsonlint-lines-primitives';
export default function readStyle(style) {
if (style instanceof String || typeof style === 'string' || style instanceof Buffer) {
try {
return jsonlint.parse(style.toString());
} catch (e) {
throw new ParsingError(e);
}
}
return style;
}

View File

@@ -0,0 +1,7 @@
// @flow
import spec from './v8.json';
export type StyleReference = typeof spec;
export default spec;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,59 @@
import path from 'path';
import replace from '@rollup/plugin-replace';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import unassert from 'rollup-plugin-unassert';
import json from '@rollup/plugin-json';
import {flow} from '../../build/rollup_plugins.js';
// Build es modules?
const esm = 'esm' in process.env;
import {fileURLToPath} from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const config = [{
input: `${__dirname}/style-spec.js`,
output: {
name: 'mapboxGlStyleSpecification',
file: `${__dirname}/dist/${esm ? 'index.es.js' : 'index.cjs'}`,
format: esm ? 'esm' : 'umd',
sourcemap: true
},
plugins: [
{
name: 'dep-checker',
resolveId(source, importer) {
// Some users reference modules within style-spec package directly, instead of the bundle
// This means that files within the style-spec package should NOT import files from the parent mapbox-gl-js tree.
// This check will cause the build to fail on CI allowing these issues to be caught.
if (importer && !importer.includes('node_modules')) {
const resolvedPath = path.join(importer, source);
const fromRoot = path.relative(__dirname, resolvedPath);
if (fromRoot.length > 2 && fromRoot.slice(0, 2) === '..') {
throw new Error(`Module ${importer} imports ${source} from outside the style-spec package root directory.`);
}
}
return null;
}
},
// https://github.com/zaach/jison/issues/351
replace({
include: /\/jsonlint-lines-primitives\/lib\/jsonlint.js/,
delimiters: ['', ''],
values: {
'_token_stack:': ''
}
}),
flow(),
json(),
unassert(),
resolve({
browser: true,
preferBuiltins: false
}),
commonjs()
]
}];
export default config;

127
node_modules/@mapbox/mapbox-gl-style-spec/style-spec.js generated vendored Normal file
View File

@@ -0,0 +1,127 @@
// @flow
type ExpressionType = 'data-driven' | 'color-ramp' | 'data-constant' | 'constant';
type ExpressionParameters = Array<'zoom' | 'feature' | 'feature-state' | 'heatmap-density' | 'line-progress' | 'sky-radial-progress' | 'pitch' | 'distance-from-center'>;
type ExpressionSpecification = {
interpolated: boolean,
parameters: ExpressionParameters
}
export type StylePropertySpecification = {
type: 'number',
'property-type': ExpressionType,
expression?: ExpressionSpecification,
transition: boolean,
default?: number
} | {
type: 'string',
'property-type': ExpressionType,
expression?: ExpressionSpecification,
transition: boolean,
default?: string,
tokens?: boolean
} | {
type: 'boolean',
'property-type': ExpressionType,
expression?: ExpressionSpecification,
transition: boolean,
default?: boolean
} | {
type: 'enum',
'property-type': ExpressionType,
expression?: ExpressionSpecification,
values: {[_: string]: {}},
transition: boolean,
default?: string
} | {
type: 'color',
'property-type': ExpressionType,
expression?: ExpressionSpecification,
transition: boolean,
default?: string,
overridable: boolean
} | {
type: 'array',
value: 'number',
'property-type': ExpressionType,
expression?: ExpressionSpecification,
length?: number,
transition: boolean,
default?: Array<number>
} | {
type: 'array',
value: 'string',
'property-type': ExpressionType,
expression?: ExpressionSpecification,
length?: number,
transition: boolean,
default?: Array<string>
} | {
type: 'resolvedImage',
'property-type': ExpressionType,
expression?: ExpressionSpecification,
transition: boolean,
default?: string
};
import v8 from './reference/v8.json';
import latest from './reference/latest.js';
import format from './format.js';
import migrate from './migrate.js';
import composite from './composite.js';
import derefLayers from './deref.js';
import diff from './diff.js';
import ValidationError from './error/validation_error.js';
import ParsingError from './error/parsing_error.js';
import {StyleExpression, isExpression, createExpression, createPropertyExpression, normalizePropertyExpression, ZoomConstantExpression, ZoomDependentExpression, StylePropertyFunction} from './expression/index.js';
import featureFilter, {isExpressionFilter} from './feature_filter/index.js';
import convertFilter from './feature_filter/convert.js';
import Color from './util/color.js';
import {createFunction, isFunction} from './function/index.js';
import convertFunction from './function/convert.js';
import {eachSource, eachLayer, eachProperty} from './visit.js';
import validate from './validate_style.js';
import validateMapboxApiSupported from './validate_mapbox_api_supported.js';
const expression = {
StyleExpression,
isExpression,
isExpressionFilter,
createExpression,
createPropertyExpression,
normalizePropertyExpression,
ZoomConstantExpression,
ZoomDependentExpression,
StylePropertyFunction
};
const styleFunction = {
convertFunction,
createFunction,
isFunction
};
const visit = {eachSource, eachLayer, eachProperty};
export {
v8,
latest,
format,
migrate,
composite,
derefLayers,
diff,
ValidationError,
ParsingError,
expression,
featureFilter,
convertFilter,
Color,
styleFunction as function,
validate,
validateMapboxApiSupported,
visit
};

31
node_modules/@mapbox/mapbox-gl-style-spec/test.js generated vendored Executable file
View File

@@ -0,0 +1,31 @@
#!/usr/bin/env node
/* eslint-disable no-process-exit */
import fs from 'fs';
import {execSync} from 'child_process';
import {createRequire} from 'module';
const packageJson = JSON.parse(fs.readFileSync('./package.json'));
process.on('unhandledRejection', error => {
// don't log `error` directly, because errors from child_process.execSync
// contain an (undocumented) `envPairs` with environment variable values
console.log(error.message || 'unhandledRejection');
process.exit(1);
});
const require = createRequire(import.meta.url);
const stylePath = require.resolve('mapbox-gl-styles/styles/basic-v9.json');
try {
for (const bin in packageJson.bin) {
const script = packageJson.bin[bin];
const command = [script, stylePath].join(' ');
console.log(command);
execSync(command).toString();
}
} catch (error) {
console.log(error.message);
process.exit(1);
}

482
node_modules/@mapbox/mapbox-gl-style-spec/types.js generated vendored Normal file
View File

@@ -0,0 +1,482 @@
// @flow
// Generated code; do not edit. Edit build/generate-flow-typed-style-spec.js instead.
/* eslint-disable */
export type ColorSpecification = string;
export type FormattedSpecification = string;
export type ResolvedImageSpecification = string;
export type PromoteIdSpecification = {[_: string]: string} | string;
export type FilterSpecification =
| ['has', string]
| ['!has', string]
| ['==', string, string | number | boolean]
| ['!=', string, string | number | boolean]
| ['>', string, string | number | boolean]
| ['>=', string, string | number | boolean]
| ['<', string, string | number | boolean]
| ['<=', string, string | number | boolean]
| Array<string | FilterSpecification>; // Can't type in, !in, all, any, none -- https://github.com/facebook/flow/issues/2443
export type TransitionSpecification = {
duration?: number,
delay?: number
};
// Note: doesn't capture interpolatable vs. non-interpolatable types.
export type CameraFunctionSpecification<T> =
| {| type: 'exponential', stops: Array<[number, T]> |}
| {| type: 'interval', stops: Array<[number, T]> |};
export type SourceFunctionSpecification<T> =
| {| type: 'exponential', stops: Array<[number, T]>, property: string, default?: T |}
| {| type: 'interval', stops: Array<[number, T]>, property: string, default?: T |}
| {| type: 'categorical', stops: Array<[string | number | boolean, T]>, property: string, default?: T |}
| {| type: 'identity', property: string, default?: T |};
export type CompositeFunctionSpecification<T> =
| {| type: 'exponential', stops: Array<[{zoom: number, value: number}, T]>, property: string, default?: T |}
| {| type: 'interval', stops: Array<[{zoom: number, value: number}, T]>, property: string, default?: T |}
| {| type: 'categorical', stops: Array<[{zoom: number, value: string | number | boolean}, T]>, property: string, default?: T |};
export type ExpressionSpecification = Array<mixed>;
export type PropertyValueSpecification<T> =
| T
| CameraFunctionSpecification<T>
| ExpressionSpecification;
export type DataDrivenPropertyValueSpecification<T> =
| T
| CameraFunctionSpecification<T>
| SourceFunctionSpecification<T>
| CompositeFunctionSpecification<T>
| ExpressionSpecification;
export type StyleSpecification = {|
"version": 8,
"name"?: string,
"metadata"?: mixed,
"center"?: Array<number>,
"zoom"?: number,
"bearing"?: number,
"pitch"?: number,
"light"?: LightSpecification,
"terrain"?: TerrainSpecification,
"fog"?: FogSpecification,
"sources": {[_: string]: SourceSpecification},
"sprite"?: string,
"glyphs"?: string,
"transition"?: TransitionSpecification,
"projection"?: ProjectionSpecification,
"layers": Array<LayerSpecification>
|}
export type LightSpecification = {|
"anchor"?: PropertyValueSpecification<"map" | "viewport">,
"position"?: PropertyValueSpecification<[number, number, number]>,
"color"?: PropertyValueSpecification<ColorSpecification>,
"intensity"?: PropertyValueSpecification<number>
|}
export type TerrainSpecification = {|
"source": string,
"exaggeration"?: PropertyValueSpecification<number>
|}
export type FogSpecification = {|
"range"?: PropertyValueSpecification<[number, number]>,
"color"?: PropertyValueSpecification<ColorSpecification>,
"high-color"?: PropertyValueSpecification<ColorSpecification>,
"space-color"?: PropertyValueSpecification<ColorSpecification>,
"horizon-blend"?: PropertyValueSpecification<number>,
"star-intensity"?: PropertyValueSpecification<number>
|}
export type ProjectionSpecification = {|
"name": "albers" | "equalEarth" | "equirectangular" | "lambertConformalConic" | "mercator" | "naturalEarth" | "winkelTripel" | "globe",
"center"?: [number, number],
"parallels"?: [number, number]
|}
export type VectorSourceSpecification = {
"type": "vector",
"url"?: string,
"tiles"?: Array<string>,
"bounds"?: [number, number, number, number],
"scheme"?: "xyz" | "tms",
"minzoom"?: number,
"maxzoom"?: number,
"attribution"?: string,
"promoteId"?: PromoteIdSpecification,
"volatile"?: boolean
}
export type RasterSourceSpecification = {
"type": "raster",
"url"?: string,
"tiles"?: Array<string>,
"bounds"?: [number, number, number, number],
"minzoom"?: number,
"maxzoom"?: number,
"tileSize"?: number,
"scheme"?: "xyz" | "tms",
"attribution"?: string,
"volatile"?: boolean
}
export type RasterDEMSourceSpecification = {
"type": "raster-dem",
"url"?: string,
"tiles"?: Array<string>,
"bounds"?: [number, number, number, number],
"minzoom"?: number,
"maxzoom"?: number,
"tileSize"?: number,
"attribution"?: string,
"encoding"?: "terrarium" | "mapbox",
"volatile"?: boolean
}
export type GeoJSONSourceSpecification = {|
"type": "geojson",
"data"?: mixed,
"maxzoom"?: number,
"attribution"?: string,
"buffer"?: number,
"filter"?: mixed,
"tolerance"?: number,
"cluster"?: boolean,
"clusterRadius"?: number,
"clusterMaxZoom"?: number,
"clusterMinPoints"?: number,
"clusterProperties"?: mixed,
"lineMetrics"?: boolean,
"generateId"?: boolean,
"promoteId"?: PromoteIdSpecification
|}
export type VideoSourceSpecification = {|
"type": "video",
"urls": Array<string>,
"coordinates": [[number, number], [number, number], [number, number], [number, number]]
|}
export type ImageSourceSpecification = {|
"type": "image",
"url": string,
"coordinates": [[number, number], [number, number], [number, number], [number, number]]
|}
export type SourceSpecification =
| VectorSourceSpecification
| RasterSourceSpecification
| RasterDEMSourceSpecification
| GeoJSONSourceSpecification
| VideoSourceSpecification
| ImageSourceSpecification
export type FillLayerSpecification = {|
"id": string,
"type": "fill",
"metadata"?: mixed,
"source": string,
"source-layer"?: string,
"minzoom"?: number,
"maxzoom"?: number,
"filter"?: FilterSpecification,
"layout"?: {|
"fill-sort-key"?: DataDrivenPropertyValueSpecification<number>,
"visibility"?: "visible" | "none"
|},
"paint"?: {|
"fill-antialias"?: PropertyValueSpecification<boolean>,
"fill-opacity"?: DataDrivenPropertyValueSpecification<number>,
"fill-color"?: DataDrivenPropertyValueSpecification<ColorSpecification>,
"fill-outline-color"?: DataDrivenPropertyValueSpecification<ColorSpecification>,
"fill-translate"?: PropertyValueSpecification<[number, number]>,
"fill-translate-anchor"?: PropertyValueSpecification<"map" | "viewport">,
"fill-pattern"?: DataDrivenPropertyValueSpecification<ResolvedImageSpecification>
|}
|}
export type LineLayerSpecification = {|
"id": string,
"type": "line",
"metadata"?: mixed,
"source": string,
"source-layer"?: string,
"minzoom"?: number,
"maxzoom"?: number,
"filter"?: FilterSpecification,
"layout"?: {|
"line-cap"?: DataDrivenPropertyValueSpecification<"butt" | "round" | "square">,
"line-join"?: DataDrivenPropertyValueSpecification<"bevel" | "round" | "miter">,
"line-miter-limit"?: PropertyValueSpecification<number>,
"line-round-limit"?: PropertyValueSpecification<number>,
"line-sort-key"?: DataDrivenPropertyValueSpecification<number>,
"visibility"?: "visible" | "none"
|},
"paint"?: {|
"line-opacity"?: DataDrivenPropertyValueSpecification<number>,
"line-color"?: DataDrivenPropertyValueSpecification<ColorSpecification>,
"line-translate"?: PropertyValueSpecification<[number, number]>,
"line-translate-anchor"?: PropertyValueSpecification<"map" | "viewport">,
"line-width"?: DataDrivenPropertyValueSpecification<number>,
"line-gap-width"?: DataDrivenPropertyValueSpecification<number>,
"line-offset"?: DataDrivenPropertyValueSpecification<number>,
"line-blur"?: DataDrivenPropertyValueSpecification<number>,
"line-dasharray"?: DataDrivenPropertyValueSpecification<Array<number>>,
"line-pattern"?: DataDrivenPropertyValueSpecification<ResolvedImageSpecification>,
"line-gradient"?: ExpressionSpecification,
"line-trim-offset"?: [number, number]
|}
|}
export type SymbolLayerSpecification = {|
"id": string,
"type": "symbol",
"metadata"?: mixed,
"source": string,
"source-layer"?: string,
"minzoom"?: number,
"maxzoom"?: number,
"filter"?: FilterSpecification,
"layout"?: {|
"symbol-placement"?: PropertyValueSpecification<"point" | "line" | "line-center">,
"symbol-spacing"?: PropertyValueSpecification<number>,
"symbol-avoid-edges"?: PropertyValueSpecification<boolean>,
"symbol-sort-key"?: DataDrivenPropertyValueSpecification<number>,
"symbol-z-order"?: PropertyValueSpecification<"auto" | "viewport-y" | "source">,
"icon-allow-overlap"?: PropertyValueSpecification<boolean>,
"icon-ignore-placement"?: PropertyValueSpecification<boolean>,
"icon-optional"?: PropertyValueSpecification<boolean>,
"icon-rotation-alignment"?: PropertyValueSpecification<"map" | "viewport" | "auto">,
"icon-size"?: DataDrivenPropertyValueSpecification<number>,
"icon-text-fit"?: PropertyValueSpecification<"none" | "width" | "height" | "both">,
"icon-text-fit-padding"?: PropertyValueSpecification<[number, number, number, number]>,
"icon-image"?: DataDrivenPropertyValueSpecification<ResolvedImageSpecification>,
"icon-rotate"?: DataDrivenPropertyValueSpecification<number>,
"icon-padding"?: PropertyValueSpecification<number>,
"icon-keep-upright"?: PropertyValueSpecification<boolean>,
"icon-offset"?: DataDrivenPropertyValueSpecification<[number, number]>,
"icon-anchor"?: DataDrivenPropertyValueSpecification<"center" | "left" | "right" | "top" | "bottom" | "top-left" | "top-right" | "bottom-left" | "bottom-right">,
"icon-pitch-alignment"?: PropertyValueSpecification<"map" | "viewport" | "auto">,
"text-pitch-alignment"?: PropertyValueSpecification<"map" | "viewport" | "auto">,
"text-rotation-alignment"?: PropertyValueSpecification<"map" | "viewport" | "auto">,
"text-field"?: DataDrivenPropertyValueSpecification<FormattedSpecification>,
"text-font"?: DataDrivenPropertyValueSpecification<Array<string>>,
"text-size"?: DataDrivenPropertyValueSpecification<number>,
"text-max-width"?: DataDrivenPropertyValueSpecification<number>,
"text-line-height"?: DataDrivenPropertyValueSpecification<number>,
"text-letter-spacing"?: DataDrivenPropertyValueSpecification<number>,
"text-justify"?: DataDrivenPropertyValueSpecification<"auto" | "left" | "center" | "right">,
"text-radial-offset"?: DataDrivenPropertyValueSpecification<number>,
"text-variable-anchor"?: PropertyValueSpecification<Array<"center" | "left" | "right" | "top" | "bottom" | "top-left" | "top-right" | "bottom-left" | "bottom-right">>,
"text-anchor"?: DataDrivenPropertyValueSpecification<"center" | "left" | "right" | "top" | "bottom" | "top-left" | "top-right" | "bottom-left" | "bottom-right">,
"text-max-angle"?: PropertyValueSpecification<number>,
"text-writing-mode"?: PropertyValueSpecification<Array<"horizontal" | "vertical">>,
"text-rotate"?: DataDrivenPropertyValueSpecification<number>,
"text-padding"?: PropertyValueSpecification<number>,
"text-keep-upright"?: PropertyValueSpecification<boolean>,
"text-transform"?: DataDrivenPropertyValueSpecification<"none" | "uppercase" | "lowercase">,
"text-offset"?: DataDrivenPropertyValueSpecification<[number, number]>,
"text-allow-overlap"?: PropertyValueSpecification<boolean>,
"text-ignore-placement"?: PropertyValueSpecification<boolean>,
"text-optional"?: PropertyValueSpecification<boolean>,
"visibility"?: "visible" | "none"
|},
"paint"?: {|
"icon-opacity"?: DataDrivenPropertyValueSpecification<number>,
"icon-color"?: DataDrivenPropertyValueSpecification<ColorSpecification>,
"icon-halo-color"?: DataDrivenPropertyValueSpecification<ColorSpecification>,
"icon-halo-width"?: DataDrivenPropertyValueSpecification<number>,
"icon-halo-blur"?: DataDrivenPropertyValueSpecification<number>,
"icon-translate"?: PropertyValueSpecification<[number, number]>,
"icon-translate-anchor"?: PropertyValueSpecification<"map" | "viewport">,
"text-opacity"?: DataDrivenPropertyValueSpecification<number>,
"text-color"?: DataDrivenPropertyValueSpecification<ColorSpecification>,
"text-halo-color"?: DataDrivenPropertyValueSpecification<ColorSpecification>,
"text-halo-width"?: DataDrivenPropertyValueSpecification<number>,
"text-halo-blur"?: DataDrivenPropertyValueSpecification<number>,
"text-translate"?: PropertyValueSpecification<[number, number]>,
"text-translate-anchor"?: PropertyValueSpecification<"map" | "viewport">
|}
|}
export type CircleLayerSpecification = {|
"id": string,
"type": "circle",
"metadata"?: mixed,
"source": string,
"source-layer"?: string,
"minzoom"?: number,
"maxzoom"?: number,
"filter"?: FilterSpecification,
"layout"?: {|
"circle-sort-key"?: DataDrivenPropertyValueSpecification<number>,
"visibility"?: "visible" | "none"
|},
"paint"?: {|
"circle-radius"?: DataDrivenPropertyValueSpecification<number>,
"circle-color"?: DataDrivenPropertyValueSpecification<ColorSpecification>,
"circle-blur"?: DataDrivenPropertyValueSpecification<number>,
"circle-opacity"?: DataDrivenPropertyValueSpecification<number>,
"circle-translate"?: PropertyValueSpecification<[number, number]>,
"circle-translate-anchor"?: PropertyValueSpecification<"map" | "viewport">,
"circle-pitch-scale"?: PropertyValueSpecification<"map" | "viewport">,
"circle-pitch-alignment"?: PropertyValueSpecification<"map" | "viewport">,
"circle-stroke-width"?: DataDrivenPropertyValueSpecification<number>,
"circle-stroke-color"?: DataDrivenPropertyValueSpecification<ColorSpecification>,
"circle-stroke-opacity"?: DataDrivenPropertyValueSpecification<number>
|}
|}
export type HeatmapLayerSpecification = {|
"id": string,
"type": "heatmap",
"metadata"?: mixed,
"source": string,
"source-layer"?: string,
"minzoom"?: number,
"maxzoom"?: number,
"filter"?: FilterSpecification,
"layout"?: {|
"visibility"?: "visible" | "none"
|},
"paint"?: {|
"heatmap-radius"?: DataDrivenPropertyValueSpecification<number>,
"heatmap-weight"?: DataDrivenPropertyValueSpecification<number>,
"heatmap-intensity"?: PropertyValueSpecification<number>,
"heatmap-color"?: ExpressionSpecification,
"heatmap-opacity"?: PropertyValueSpecification<number>
|}
|}
export type FillExtrusionLayerSpecification = {|
"id": string,
"type": "fill-extrusion",
"metadata"?: mixed,
"source": string,
"source-layer"?: string,
"minzoom"?: number,
"maxzoom"?: number,
"filter"?: FilterSpecification,
"layout"?: {|
"visibility"?: "visible" | "none",
"fill-extrusion-edge-radius"?: number
|},
"paint"?: {|
"fill-extrusion-opacity"?: PropertyValueSpecification<number>,
"fill-extrusion-color"?: DataDrivenPropertyValueSpecification<ColorSpecification>,
"fill-extrusion-translate"?: PropertyValueSpecification<[number, number]>,
"fill-extrusion-translate-anchor"?: PropertyValueSpecification<"map" | "viewport">,
"fill-extrusion-pattern"?: DataDrivenPropertyValueSpecification<ResolvedImageSpecification>,
"fill-extrusion-height"?: DataDrivenPropertyValueSpecification<number>,
"fill-extrusion-base"?: DataDrivenPropertyValueSpecification<number>,
"fill-extrusion-vertical-gradient"?: PropertyValueSpecification<boolean>,
"fill-extrusion-ambient-occlusion-intensity"?: PropertyValueSpecification<number>,
"fill-extrusion-ambient-occlusion-radius"?: PropertyValueSpecification<number>
|}
|}
export type RasterLayerSpecification = {|
"id": string,
"type": "raster",
"metadata"?: mixed,
"source": string,
"source-layer"?: string,
"minzoom"?: number,
"maxzoom"?: number,
"filter"?: FilterSpecification,
"layout"?: {|
"visibility"?: "visible" | "none"
|},
"paint"?: {|
"raster-opacity"?: PropertyValueSpecification<number>,
"raster-hue-rotate"?: PropertyValueSpecification<number>,
"raster-brightness-min"?: PropertyValueSpecification<number>,
"raster-brightness-max"?: PropertyValueSpecification<number>,
"raster-saturation"?: PropertyValueSpecification<number>,
"raster-contrast"?: PropertyValueSpecification<number>,
"raster-resampling"?: PropertyValueSpecification<"linear" | "nearest">,
"raster-fade-duration"?: PropertyValueSpecification<number>
|}
|}
export type HillshadeLayerSpecification = {|
"id": string,
"type": "hillshade",
"metadata"?: mixed,
"source": string,
"source-layer"?: string,
"minzoom"?: number,
"maxzoom"?: number,
"filter"?: FilterSpecification,
"layout"?: {|
"visibility"?: "visible" | "none"
|},
"paint"?: {|
"hillshade-illumination-direction"?: PropertyValueSpecification<number>,
"hillshade-illumination-anchor"?: PropertyValueSpecification<"map" | "viewport">,
"hillshade-exaggeration"?: PropertyValueSpecification<number>,
"hillshade-shadow-color"?: PropertyValueSpecification<ColorSpecification>,
"hillshade-highlight-color"?: PropertyValueSpecification<ColorSpecification>,
"hillshade-accent-color"?: PropertyValueSpecification<ColorSpecification>
|}
|}
export type BackgroundLayerSpecification = {|
"id": string,
"type": "background",
"metadata"?: mixed,
"minzoom"?: number,
"maxzoom"?: number,
"layout"?: {|
"visibility"?: "visible" | "none"
|},
"paint"?: {|
"background-color"?: PropertyValueSpecification<ColorSpecification>,
"background-pattern"?: PropertyValueSpecification<ResolvedImageSpecification>,
"background-opacity"?: PropertyValueSpecification<number>
|}
|}
export type SkyLayerSpecification = {|
"id": string,
"type": "sky",
"metadata"?: mixed,
"minzoom"?: number,
"maxzoom"?: number,
"layout"?: {|
"visibility"?: "visible" | "none"
|},
"paint"?: {|
"sky-type"?: PropertyValueSpecification<"gradient" | "atmosphere">,
"sky-atmosphere-sun"?: PropertyValueSpecification<[number, number]>,
"sky-atmosphere-sun-intensity"?: number,
"sky-gradient-center"?: PropertyValueSpecification<[number, number]>,
"sky-gradient-radius"?: PropertyValueSpecification<number>,
"sky-gradient"?: ExpressionSpecification,
"sky-atmosphere-halo-color"?: ColorSpecification,
"sky-atmosphere-color"?: ColorSpecification,
"sky-opacity"?: PropertyValueSpecification<number>
|}
|}
export type LayerSpecification =
| FillLayerSpecification
| LineLayerSpecification
| SymbolLayerSpecification
| CircleLayerSpecification
| HeatmapLayerSpecification
| FillExtrusionLayerSpecification
| RasterLayerSpecification
| HillshadeLayerSpecification
| BackgroundLayerSpecification
| SkyLayerSpecification;

132
node_modules/@mapbox/mapbox-gl-style-spec/util/color.js generated vendored Normal file
View File

@@ -0,0 +1,132 @@
// @flow
import {parseCSSColor} from 'csscolorparser';
/**
* An RGBA color value. Create instances from color strings using the static
* method `Color.parse`. The constructor accepts RGB channel values in the range
* `[0, 1]`, premultiplied by A.
*
* @param {number} r The red channel.
* @param {number} g The green channel.
* @param {number} b The blue channel.
* @param {number} a The alpha channel.
* @private
*/
class Color {
r: number;
g: number;
b: number;
a: number;
constructor(r: number, g: number, b: number, a: number = 1) {
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
static black: Color;
static white: Color;
static transparent: Color;
static red: Color;
static blue: Color;
/**
* Parses valid CSS color strings and returns a `Color` instance.
* @returns A `Color` instance, or `undefined` if the input is not a valid color string.
*/
static parse(input?: string | Color | null): Color | void {
if (!input) {
return undefined;
}
if (input instanceof Color) {
return input;
}
if (typeof input !== 'string') {
return undefined;
}
const rgba = parseCSSColor(input);
if (!rgba) {
return undefined;
}
return new Color(
rgba[0] / 255 * rgba[3],
rgba[1] / 255 * rgba[3],
rgba[2] / 255 * rgba[3],
rgba[3]
);
}
/**
* Returns an RGBA string representing the color value.
*
* @returns An RGBA string.
* @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(): string {
const [r, g, b, a] = this.toArray();
return `rgba(${Math.round(r)},${Math.round(g)},${Math.round(b)},${a})`;
}
/**
* Returns an RGBA array of values representing the color, unpremultiplied by A.
*
* @returns An array of RGBA color values in the range [0, 255].
*/
toArray(): [number, number, number, number] {
const {r, g, b, a} = this;
return a === 0 ? [0, 0, 0, 0] : [
r * 255 / a,
g * 255 / a,
b * 255 / a,
a
];
}
/**
* Returns a RGBA array of float values representing the color, unpremultiplied by A.
*
* @returns An array of RGBA color values in the range [0, 1].
*/
toArray01(): [number, number, number, number] {
const {r, g, b, a} = this;
return a === 0 ? [0, 0, 0, 0] : [
r / a,
g / a,
b / a,
a
];
}
/**
* Returns an RGBA array of values representing the color, premultiplied by A.
*
* @returns An array of RGBA color values in the range [0, 1].
*/
toArray01PremultipliedAlpha(): [number, number, number, number] {
const {r, g, b, a} = this;
return [
r,
g,
b,
a
];
}
}
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);
Color.blue = new Color(0, 0, 1, 1);
export default Color;

View File

@@ -0,0 +1,139 @@
// @flow
import Color from './color.js';
import {number as interpolateNumber} from './interpolate.js';
type LABColor = {
l: number,
a: number,
b: number,
alpha: number
};
type HCLColor = {
h: number,
c: number,
l: number,
alpha: number
};
// Constants
const Xn = 0.950470, // D65 standard referent
Yn = 1,
Zn = 1.088830,
t0 = 4 / 29,
t1 = 6 / 29,
t2 = 3 * t1 * t1,
t3 = t1 * t1 * t1,
deg2rad = Math.PI / 180,
rad2deg = 180 / Math.PI;
// Utilities
function xyz2lab(t: number) {
return t > t3 ? Math.pow(t, 1 / 3) : t / t2 + t0;
}
function lab2xyz(t: number) {
return t > t1 ? t * t * t : t2 * (t - t0);
}
function xyz2rgb(x: number) {
return 255 * (x <= 0.0031308 ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055);
}
function rgb2xyz(x: number) {
x /= 255;
return x <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
}
// LAB
function rgbToLab(rgbColor: Color): LABColor {
const b = rgb2xyz(rgbColor.r),
a = rgb2xyz(rgbColor.g),
l = rgb2xyz(rgbColor.b),
x = xyz2lab((0.4124564 * b + 0.3575761 * a + 0.1804375 * l) / Xn),
y = xyz2lab((0.2126729 * b + 0.7151522 * a + 0.0721750 * l) / Yn),
z = xyz2lab((0.0193339 * b + 0.1191920 * a + 0.9503041 * l) / Zn);
return {
l: 116 * y - 16,
a: 500 * (x - y),
b: 200 * (y - z),
alpha: rgbColor.a
};
}
function labToRgb(labColor: LABColor): Color {
let y = (labColor.l + 16) / 116,
x = isNaN(labColor.a) ? y : y + labColor.a / 500,
z = isNaN(labColor.b) ? y : y - labColor.b / 200;
y = Yn * lab2xyz(y);
x = Xn * lab2xyz(x);
z = Zn * lab2xyz(z);
return new Color(
xyz2rgb(3.2404542 * x - 1.5371385 * y - 0.4985314 * z), // D65 -> sRGB
xyz2rgb(-0.9692660 * x + 1.8760108 * y + 0.0415560 * z),
xyz2rgb(0.0556434 * x - 0.2040259 * y + 1.0572252 * z),
labColor.alpha
);
}
function interpolateLab(from: LABColor, to: LABColor, t: number): LABColor {
return {
l: interpolateNumber(from.l, to.l, t),
a: interpolateNumber(from.a, to.a, t),
b: interpolateNumber(from.b, to.b, t),
alpha: interpolateNumber(from.alpha, to.alpha, t)
};
}
// HCL
function rgbToHcl(rgbColor: Color): HCLColor {
const {l, a, b} = rgbToLab(rgbColor);
const h = Math.atan2(b, a) * rad2deg;
return {
h: h < 0 ? h + 360 : h,
c: Math.sqrt(a * a + b * b),
l,
alpha: rgbColor.a
};
}
function hclToRgb(hclColor: HCLColor): Color {
const h = hclColor.h * deg2rad,
c = hclColor.c,
l = hclColor.l;
return labToRgb({
l,
a: Math.cos(h) * c,
b: Math.sin(h) * c,
alpha: hclColor.alpha
});
}
function interpolateHue(a: number, b: number, t: number) {
const d = b - a;
return a + t * (d > 180 || d < -180 ? d - 360 * Math.round(d / 360) : d);
}
function interpolateHcl(from: HCLColor, to: HCLColor, t: number): HCLColor {
return {
h: interpolateHue(from.h, to.h, t),
c: interpolateNumber(from.c, to.c, t),
l: interpolateNumber(from.l, to.l, t),
alpha: interpolateNumber(from.alpha, to.alpha, t)
};
}
export const lab = {
forward: rgbToLab,
reverse: labToRgb,
interpolate: interpolateLab
};
export const hcl = {
forward: rgbToHcl,
reverse: hclToRgb,
interpolate: interpolateHcl
};

View File

@@ -0,0 +1,28 @@
// @flow
/**
* Deeply compares two object literals.
*
* @private
*/
function deepEqual(a: ?mixed, b: ?mixed): boolean {
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;
}
export default deepEqual;

View File

@@ -0,0 +1,10 @@
// @flow
export default function (output: any, ...inputs: Array<any>): any {
for (const input of inputs) {
for (const k in input) {
output[k] = input[k];
}
}
return output;
}

View File

@@ -0,0 +1,17 @@
// @flow
export default function getType(val: mixed): string {
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;
}
}

View File

@@ -0,0 +1,22 @@
// @flow
import Color from './color.js';
export function number(a: number, b: number, t: number): number {
return (a * (1 - t)) + (b * t);
}
export function color(from: Color, to: Color, t: number): Color {
return new Color(
number(from.r, to.r, t),
number(from.g, to.g, t),
number(from.b, to.b, t),
number(from.a, to.a, t)
);
}
export function array(from: Array<number>, to: Array<number>, t: number): Array<number> {
return from.map((d, i) => {
return number(d, to[i], t);
});
}

View File

@@ -0,0 +1,15 @@
// @flow
import type {StylePropertySpecification} from '../style-spec.js';
export function supportsPropertyExpression(spec: StylePropertySpecification): boolean {
return spec['property-type'] === 'data-driven';
}
export function supportsZoomExpression(spec: StylePropertySpecification): boolean {
return !!spec.expression && spec.expression.parameters.indexOf('zoom') > -1;
}
export function supportsInterpolation(spec: StylePropertySpecification): boolean {
return !!spec.expression && spec.expression.interpolated;
}

View File

@@ -0,0 +1,2 @@
// @flow
export default ['type', 'source', 'source-layer', 'minzoom', 'maxzoom', 'filter', 'layout'];

View File

@@ -0,0 +1,19 @@
// @flow
/**
* A type used for returning and propagating errors. The first element of the union
* represents success and contains a value, and the second represents an error and
* contains an error value.
* @private
*/
export type Result<T, E> =
| {| result: 'success', value: T |}
| {| result: 'error', value: E |};
export function success<T, E>(value: T): Result<T, E> {
return {result: 'success', value};
}
export function error<T, E>(value: E): Result<T, E> {
return {result: 'error', value};
}

View File

@@ -0,0 +1,24 @@
// @flow
// Turn jsonlint-lines-primitives objects into primitive objects
export function unbundle(value: mixed): mixed {
if (value instanceof Number || value instanceof String || value instanceof Boolean) {
return value.valueOf();
} else {
return value;
}
}
export function deepUnbundle(value: mixed): mixed {
if (Array.isArray(value)) {
return value.map(deepUnbundle);
} else if (value instanceof Object && !(value instanceof Number || value instanceof String || value instanceof Boolean)) {
const unbundledValue: { [key: string]: mixed } = {};
for (const key in value) {
unbundledValue[key] = deepUnbundle(value[key]);
}
return unbundledValue;
}
return unbundle(value);
}

View File

@@ -0,0 +1,91 @@
// @flow
import extend from '../util/extend.js';
import {unbundle, deepUnbundle} from '../util/unbundle_jsonlint.js';
import {isExpression} from '../expression/index.js';
import {isFunction} from '../function/index.js';
import validateFunction from './validate_function.js';
import validateExpression from './validate_expression.js';
import validateObject from './validate_object.js';
import validateArray from './validate_array.js';
import validateBoolean from './validate_boolean.js';
import validateNumber from './validate_number.js';
import validateColor from './validate_color.js';
import validateEnum from './validate_enum.js';
import validateFilter from './validate_filter.js';
import validateLayer from './validate_layer.js';
import validateSource from './validate_source.js';
import validateLight from './validate_light.js';
import validateTerrain from './validate_terrain.js';
import validateFog from './validate_fog.js';
import validateString from './validate_string.js';
import validateFormatted from './validate_formatted.js';
import validateImage from './validate_image.js';
import validateProjection from './validate_projection.js';
import type {StyleReference} from '../reference/latest.js';
import type {StyleSpecification} from '../types.js';
import type ValidationError from '../error/validation_error.js';
const VALIDATORS = {
'*'() {
return [];
},
'array': validateArray,
'boolean': validateBoolean,
'number': validateNumber,
'color': validateColor,
'enum': validateEnum,
'filter': validateFilter,
'function': validateFunction,
'layer': validateLayer,
'object': validateObject,
'source': validateSource,
'light': validateLight,
'terrain': validateTerrain,
'fog': validateFog,
'string': validateString,
'formatted': validateFormatted,
'resolvedImage': validateImage,
'projection': validateProjection
};
// Main recursive validation function. Tracks:
//
// - key: string representing location of validation in style tree. Used only
// for more informative error reporting.
// - 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.
// - valueSpec: current spec being evaluated. Tracks value.
// - styleSpec: current full spec being evaluated.
export type ValidationOptions = {
key: string;
value: Object;
valueSpec: Object;
style: $Shape<StyleSpecification>;
styleSpec: StyleReference;
}
export default function validate(options: ValidationOptions): Array<ValidationError> {
const value = options.value;
const valueSpec = options.valueSpec;
const styleSpec = options.styleSpec;
if (valueSpec.expression && isFunction(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(extend({}, options, {
valueSpec: valueSpec.type ? styleSpec[valueSpec.type] : valueSpec
}));
return valid;
}
}

Some files were not shown because too many files have changed in this diff Show More