planning
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
View File
@@ -0,0 +1,10 @@
{
"overrides": [
{
"files": ["rollup.config.js", "test.js"],
"rules": {
"flowtype/require-valid-file-annotation": "off"
}
}
]
}
+564
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
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.
+25
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');
}
+30
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.');
}
+25
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');
}
+57
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
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
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
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
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+10
View File
@@ -0,0 +1,10 @@
// @flow
import type {StyleSpecification} from './types.js';
export default function emptyStyle(): StyleSpecification {
return {
version: 8,
layers: [],
sources: {}
};
}
+16
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;
}
}
+18
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__;
}
}
}
@@ -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;
@@ -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;
+70
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;
@@ -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;
@@ -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;
@@ -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;
@@ -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];
}
}
@@ -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);
@@ -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;
}
}
@@ -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()];
}
}
+72
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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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];
}
}
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
+27
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};
+399
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;
}
}
+59
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};
+233
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']);
}
+13
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;
+17
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;
+36
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;
+39
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;
}
+126
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;
}
});
}
+61
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;
}
}
@@ -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;
}
}
@@ -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];
}
}
+123
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};
+55
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);
```
+208
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];
}
}
+336
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];
}
+43
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;
}
+113
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
}
}
+5
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;
}
+13
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>;
}
+16
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
};
}
@@ -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;
}
}
@@ -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;
}
@@ -0,0 +1,9 @@
// @flow strict
declare class OffscreenCanvas {
width: number;
height: number;
constructor(width: number, height: number): OffscreenCanvas;
getContext(contextType: '2d'): CanvasRenderingContext2D;
}
+26
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>;
}
+46
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>;
}
+13
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;
}
+28
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;
}
+31
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>;
}
+49
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
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;
+270
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;
}
+262
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);
}
}
+73
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
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;
}
+39
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
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;
}
+26
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
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
}
+14
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;
}
+7
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
+59
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
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
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
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
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;
+139
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
};
+28
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;
+10
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;
}
+17
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;
}
}
+22
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);
});
}
+15
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;
}
+2
View File
@@ -0,0 +1,2 @@
// @flow
export default ['type', 'source', 'source-layer', 'minzoom', 'maxzoom', 'filter', 'layout'];
+19
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};
}
+24
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);
}
+91
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