@mapbox/mapbox-gl-style-spec 13.22.0-beta.1 → 13.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -201,6 +201,16 @@ CompoundExpression.register(expressions, {
201
201
  [],
202
202
  (ctx) => ctx.globals.zoom
203
203
  ],
204
+ 'pitch': [
205
+ NumberType,
206
+ [],
207
+ (ctx) => ctx.globals.pitch || 0
208
+ ],
209
+ 'distance-from-center': [
210
+ NumberType,
211
+ [],
212
+ (ctx) => ctx.distanceFromCenter()
213
+ ],
204
214
  'heatmap-density': [
205
215
  NumberType,
206
216
  [],
@@ -1,9 +1,12 @@
1
1
  // @flow
2
2
 
3
3
  import {Color} from './values.js';
4
+
5
+ import type Point from '@mapbox/point-geometry';
4
6
  import type {FormattedSection} from './types/formatted.js';
5
7
  import type {GlobalProperties, Feature, FeatureState} from './index.js';
6
8
  import type {CanonicalTileID} from '../../source/tile_id.js';
9
+ import type {FeatureDistanceData} from '../feature_filter/index.js';
7
10
 
8
11
  const geometryTypes = ['Unknown', 'Point', 'LineString', 'Polygon'];
9
12
 
@@ -14,6 +17,8 @@ class EvaluationContext {
14
17
  formattedSection: ?FormattedSection;
15
18
  availableImages: ?Array<string>;
16
19
  canonical: ?CanonicalTileID;
20
+ featureTileCoord: ?Point;
21
+ featureDistanceData: ?FeatureDistanceData;
17
22
 
18
23
  _parseColorCache: {[_: string]: ?Color};
19
24
 
@@ -25,6 +30,8 @@ class EvaluationContext {
25
30
  this._parseColorCache = {};
26
31
  this.availableImages = null;
27
32
  this.canonical = null;
33
+ this.featureTileCoord = null;
34
+ this.featureDistanceData = null;
28
35
  }
29
36
 
30
37
  id() {
@@ -47,6 +54,29 @@ class EvaluationContext {
47
54
  return this.feature && this.feature.properties || {};
48
55
  }
49
56
 
57
+ distanceFromCenter() {
58
+ if (this.featureTileCoord && this.featureDistanceData) {
59
+
60
+ const c = this.featureDistanceData.center;
61
+ const scale = this.featureDistanceData.scale;
62
+ const {x, y} = this.featureTileCoord;
63
+
64
+ // Calculate the distance vector `d` (left handed)
65
+ const dX = x * scale - c[0];
66
+ const dY = y * scale - c[1];
67
+
68
+ // The bearing vector `b` (left handed)
69
+ const bX = this.featureDistanceData.bearing[0];
70
+ const bY = this.featureDistanceData.bearing[1];
71
+
72
+ // Distance is calculated as `dot(d, v)`
73
+ const dist = (bX * dX + bY * dY);
74
+ return dist;
75
+ }
76
+
77
+ return 0;
78
+ }
79
+
50
80
  parseColor(input: string): ?Color {
51
81
  let cached = this._parseColorCache[input];
52
82
  if (!cached) {
@@ -27,6 +27,7 @@ import type {PropertyValueSpecification} from '../types.js';
27
27
  import type {FormattedSection} from './types/formatted.js';
28
28
  import type Point from '@mapbox/point-geometry';
29
29
  import type {CanonicalTileID} from '../../source/tile_id.js';
30
+ import type {FeatureDistanceData} from '../feature_filter/index.js';
30
31
 
31
32
  export type Feature = {
32
33
  +type: 1 | 2 | 3 | 'Unknown' | 'Point' | 'MultiPoint' | 'LineString' | 'MultiLineString' | 'Polygon' | 'MultiPolygon',
@@ -40,6 +41,7 @@ export type FeatureState = {[_: string]: any};
40
41
 
41
42
  export type GlobalProperties = $ReadOnly<{
42
43
  zoom: number,
44
+ pitch?: number,
43
45
  heatmapDensity?: number,
44
46
  lineProgress?: number,
45
47
  skyRadialProgress?: number,
@@ -63,24 +65,28 @@ export class StyleExpression {
63
65
  this._enumValues = propertySpec && propertySpec.type === 'enum' ? propertySpec.values : null;
64
66
  }
65
67
 
66
- evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection): any {
68
+ evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection, featureTileCoord?: Point, featureDistanceData?: FeatureDistanceData): any {
67
69
  this._evaluator.globals = globals;
68
70
  this._evaluator.feature = feature;
69
71
  this._evaluator.featureState = featureState;
70
72
  this._evaluator.canonical = canonical;
71
73
  this._evaluator.availableImages = availableImages || null;
72
74
  this._evaluator.formattedSection = formattedSection;
75
+ this._evaluator.featureTileCoord = featureTileCoord || null;
76
+ this._evaluator.featureDistanceData = featureDistanceData || null;
73
77
 
74
78
  return this.expression.evaluate(this._evaluator);
75
79
  }
76
80
 
77
- evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection): any {
81
+ evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection, featureTileCoord?: Point, featureDistanceData?: FeatureDistanceData): any {
78
82
  this._evaluator.globals = globals;
79
83
  this._evaluator.feature = feature || null;
80
84
  this._evaluator.featureState = featureState || null;
81
85
  this._evaluator.canonical = canonical;
82
86
  this._evaluator.availableImages = availableImages || null;
83
87
  this._evaluator.formattedSection = formattedSection || null;
88
+ this._evaluator.featureTileCoord = featureTileCoord || null;
89
+ this._evaluator.featureDistanceData = featureDistanceData || null;
84
90
 
85
91
  try {
86
92
  const val = this.expression.evaluate(this._evaluator);
@@ -233,7 +239,7 @@ export function createPropertyExpression(expression: mixed, propertySpec: StyleP
233
239
  return error([new ParsingError('', 'data expressions not supported')]);
234
240
  }
235
241
 
236
- const isZoomConstant = isConstant.isGlobalPropertyConstant(parsed, ['zoom']);
242
+ const isZoomConstant = isConstant.isGlobalPropertyConstant(parsed, ['zoom', 'pitch', 'distance-from-center']);
237
243
  if (!isZoomConstant && !supportsZoomExpression(propertySpec)) {
238
244
  return error([new ParsingError('', 'zoom expressions not supported')]);
239
245
  }
@@ -229,5 +229,5 @@ function isConstant(expression: Expression) {
229
229
  }
230
230
 
231
231
  return isFeatureConstant(expression) &&
232
- isGlobalPropertyConstant(expression, ['zoom', 'heatmap-density', 'line-progress', 'sky-radial-progress', 'accumulated', 'is-supported-script']);
232
+ isGlobalPropertyConstant(expression, ['zoom', 'heatmap-density', 'line-progress', 'sky-radial-progress', 'accumulated', 'is-supported-script', 'pitch', 'distance-from-center']);
233
233
  }
@@ -1,14 +1,19 @@
1
1
  // @flow
2
2
 
3
3
  import {createExpression} from '../expression/index.js';
4
+ import {isFeatureConstant} from '../expression/is_constant.js';
5
+ import {deepUnbundle} from '../util/unbundle_jsonlint.js';
6
+ import latest from '../reference/latest.js';
4
7
  import type {GlobalProperties, Feature} from '../expression/index.js';
5
8
  import type {CanonicalTileID} from '../../source/tile_id.js';
9
+ import type Point from '@mapbox/point-geometry';
6
10
 
7
- type FilterExpression = (globalProperties: GlobalProperties, feature: Feature, canonical?: CanonicalTileID) => boolean;
8
- export type FeatureFilter ={filter: FilterExpression, needGeometry: boolean};
11
+ export type FeatureDistanceData = {bearing: [number, number], center: [number, number], scale: number};
12
+ type FilterExpression = (globalProperties: GlobalProperties, feature: Feature, canonical?: CanonicalTileID, featureTileCoord?: Point, featureDistanceData?: FeatureDistanceData) => boolean;
13
+ export type FeatureFilter = {filter: FilterExpression, dynamicFilter?: FilterExpression, needGeometry: boolean, needFeature: boolean};
9
14
 
10
15
  export default createFilter;
11
- export {isExpressionFilter};
16
+ export {isExpressionFilter, isDynamicFilter, extractStaticFilter};
12
17
 
13
18
  function isExpressionFilter(filter: any) {
14
19
  if (filter === true || filter === false) {
@@ -52,17 +57,6 @@ function isExpressionFilter(filter: any) {
52
57
  }
53
58
  }
54
59
 
55
- const filterSpec = {
56
- 'type': 'boolean',
57
- 'default': false,
58
- 'transition': false,
59
- 'property-type': 'data-driven',
60
- 'expression': {
61
- 'interpolated': false,
62
- 'parameters': ['zoom', 'feature']
63
- }
64
- };
65
-
66
60
  /**
67
61
  * Given a filter expressed as nested arrays, return a new function
68
62
  * that evaluates whether a given feature (with a .properties or .tags property)
@@ -70,25 +64,192 @@ const filterSpec = {
70
64
  *
71
65
  * @private
72
66
  * @param {Array} filter mapbox gl filter
67
+ * @param {string} layerType the type of the layer this filter will be applied to.
73
68
  * @returns {Function} filter-evaluating function
74
69
  */
75
- function createFilter(filter: any): FeatureFilter {
70
+ function createFilter(filter: any, layerType?: string = 'fill'): FeatureFilter {
76
71
  if (filter === null || filter === undefined) {
77
- return {filter: () => true, needGeometry: false};
72
+ return {filter: () => true, needGeometry: false, needFeature: false};
78
73
  }
79
74
 
80
75
  if (!isExpressionFilter(filter)) {
81
76
  filter = convertFilter(filter);
82
77
  }
78
+ const filterExp = ((filter: any): string[] | string | boolean);
79
+
80
+ let staticFilter = true;
81
+ try {
82
+ staticFilter = extractStaticFilter(filterExp);
83
+ } catch (e) {
84
+ console.warn(
85
+ `Failed to extract static filter. Filter will continue working, but at higher memory usage and slower framerate.
86
+ 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
87
+ and paste the contents of this message in the report.
88
+ Thank you!
89
+ Filter Expression:
90
+ ${JSON.stringify(filterExp, null, 2)}
91
+ `);
92
+ }
83
93
 
84
- const compiled = createExpression(filter, filterSpec);
85
- if (compiled.result === 'error') {
86
- throw new Error(compiled.value.map(err => `${err.key}: ${err.message}`).join(', '));
94
+ // Compile the static component of the filter
95
+ const filterSpec = latest[`filter_${layerType}`];
96
+ const compiledStaticFilter = createExpression(staticFilter, filterSpec);
97
+
98
+ let filterFunc = null;
99
+ if (compiledStaticFilter.result === 'error') {
100
+ throw new Error(compiledStaticFilter.value.map(err => `${err.key}: ${err.message}`).join(', '));
87
101
  } else {
88
- const needGeometry = geometryNeeded(filter);
89
- return {filter: (globalProperties: GlobalProperties, feature: Feature, canonical?: CanonicalTileID) => compiled.value.evaluate(globalProperties, feature, {}, canonical),
90
- needGeometry};
102
+ filterFunc = (globalProperties: GlobalProperties, feature: Feature, canonical?: CanonicalTileID) => compiledStaticFilter.value.evaluate(globalProperties, feature, {}, canonical);
103
+ }
104
+
105
+ // If the static component is not equal to the entire filter then we have a dynamic component
106
+ // Compile the dynamic component separately
107
+ let dynamicFilterFunc = null;
108
+ let needFeature = null;
109
+ if (staticFilter !== filterExp) {
110
+ const compiledDynamicFilter = createExpression(filterExp, filterSpec);
111
+
112
+ if (compiledDynamicFilter.result === 'error') {
113
+ throw new Error(compiledDynamicFilter.value.map(err => `${err.key}: ${err.message}`).join(', '));
114
+ } else {
115
+ dynamicFilterFunc = (globalProperties: GlobalProperties, feature: Feature, canonical?: CanonicalTileID, featureTileCoord?: Point, featureDistanceData?: FeatureDistanceData) => compiledDynamicFilter.value.evaluate(globalProperties, feature, {}, canonical, undefined, undefined, featureTileCoord, featureDistanceData);
116
+ needFeature = !isFeatureConstant(compiledDynamicFilter.value.expression);
117
+ }
118
+ }
119
+
120
+ filterFunc = ((filterFunc: any): FilterExpression);
121
+ const needGeometry = geometryNeeded(staticFilter);
122
+
123
+ return {
124
+ filter: filterFunc,
125
+ dynamicFilter: dynamicFilterFunc ? dynamicFilterFunc : undefined,
126
+ needGeometry,
127
+ needFeature: !!needFeature
128
+ };
129
+ }
130
+
131
+ function extractStaticFilter(filter: any): any {
132
+ if (!isDynamicFilter(filter)) {
133
+ return filter;
134
+ }
135
+
136
+ // Shallow copy so we can replace expressions in-place
137
+ let result = deepUnbundle(filter);
138
+
139
+ // 1. Union branches
140
+ unionDynamicBranches(result);
141
+
142
+ // 2. Collapse dynamic conditions to `true`
143
+ result = collapseDynamicBooleanExpressions(result);
144
+
145
+ return result;
146
+ }
147
+
148
+ function collapseDynamicBooleanExpressions(expression: any): any {
149
+ if (!Array.isArray(expression)) {
150
+ return expression;
151
+ }
152
+
153
+ const collapsed = collapsedExpression(expression);
154
+ if (collapsed === true) {
155
+ return collapsed;
156
+ } else {
157
+ return collapsed.map((subExpression) => collapseDynamicBooleanExpressions(subExpression));
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Traverses the expression and replaces all instances of branching on a
163
+ * `dynamic` conditional (such as `['pitch']` or `['distance-from-center']`)
164
+ * into an `any` expression.
165
+ * This ensures that all possible outcomes of a `dynamic` branch are considered
166
+ * when evaluating the expression upfront during filtering.
167
+ *
168
+ * @param {Array<any>} filter the filter expression mutated in-place.
169
+ */
170
+ function unionDynamicBranches(filter: any) {
171
+ let isBranchingDynamically = false;
172
+ const branches = [];
173
+
174
+ if (filter[0] === 'case') {
175
+ for (let i = 1; i < filter.length - 1; i += 2) {
176
+ isBranchingDynamically = isBranchingDynamically || isDynamicFilter(filter[i]);
177
+ branches.push(filter[i + 1]);
178
+ }
179
+
180
+ branches.push(filter[filter.length - 1]);
181
+ } else if (filter[0] === 'match') {
182
+ isBranchingDynamically = isBranchingDynamically || isDynamicFilter(filter[1]);
183
+
184
+ for (let i = 2; i < filter.length - 1; i += 2) {
185
+ branches.push(filter[i + 1]);
186
+ }
187
+ branches.push(filter[filter.length - 1]);
188
+ } else if (filter[0] === 'step') {
189
+ isBranchingDynamically = isBranchingDynamically || isDynamicFilter(filter[1]);
190
+
191
+ for (let i = 1; i < filter.length - 1; i += 2) {
192
+ branches.push(filter[i + 1]);
193
+ }
194
+ }
195
+
196
+ if (isBranchingDynamically) {
197
+ filter.length = 0;
198
+ filter.push('any', ...branches);
199
+ }
200
+
201
+ // traverse and recurse into children
202
+ for (let i = 1; i < filter.length; i++) {
203
+ unionDynamicBranches(filter[i]);
204
+ }
205
+ }
206
+
207
+ function isDynamicFilter(filter: any): boolean {
208
+ // Base Cases
209
+ if (!Array.isArray(filter)) {
210
+ return false;
211
+ }
212
+ if (isRootExpressionDynamic(filter[0])) {
213
+ return true;
214
+ }
215
+
216
+ for (let i = 1; i < filter.length; i++) {
217
+ const child = filter[i];
218
+ if (isDynamicFilter(child)) {
219
+ return true;
220
+ }
221
+ }
222
+
223
+ return false;
224
+ }
225
+
226
+ function isRootExpressionDynamic(expression: string): boolean {
227
+ return expression === 'pitch' ||
228
+ expression === 'distance-from-center';
229
+ }
230
+
231
+ const dynamicConditionExpressions = new Set([
232
+ 'in',
233
+ '==',
234
+ '!=',
235
+ '>',
236
+ '>=',
237
+ '<',
238
+ '<=',
239
+ 'to-boolean'
240
+ ]);
241
+
242
+ function collapsedExpression(expression: any): any {
243
+ if (dynamicConditionExpressions.has(expression[0])) {
244
+
245
+ for (let i = 1; i < expression.length; i++) {
246
+ const param = expression[i];
247
+ if (isDynamicFilter(param)) {
248
+ return true;
249
+ }
250
+ }
91
251
  }
252
+ return expression;
92
253
  }
93
254
 
94
255
  // Comparison function to sort numbers and strings
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mapbox/mapbox-gl-style-spec",
3
3
  "description": "a specification for mapbox gl styles",
4
- "version": "13.22.0-beta.1",
4
+ "version": "13.23.0",
5
5
  "author": "Mapbox",
6
6
  "keywords": [
7
7
  "mapbox",