@mapbox/mapbox-gl-style-spec
Advanced tools
Comparing version 13.22.0 to 13.23.0-beta.1
@@ -1,5 +0,14 @@ | ||
# 13.22.0-beta.1 | ||
# 13.23.0-beta.1 | ||
### ✨ 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 | ||
@@ -6,0 +15,0 @@ |
@@ -109,4 +109,8 @@ | ||
*/ | ||
setFog: 'setFog' | ||
setFog: 'setFog', | ||
/* | ||
* { command: 'setProjection', args: [projectionProperties] } | ||
*/ | ||
setProjection: 'setProjection' | ||
}; | ||
@@ -367,2 +371,5 @@ | ||
} | ||
if (!isEqual(before.projection, after.projection)) { | ||
commands.push({command: operations.setProjection, args: [after.projection]}); | ||
} | ||
@@ -369,0 +376,0 @@ // Handle changes to `sources` |
@@ -204,2 +204,12 @@ // @flow | ||
], | ||
'pitch': [ | ||
NumberType, | ||
[], | ||
(ctx) => ctx.globals.pitch || 0 | ||
], | ||
'distance-from-center': [ | ||
NumberType, | ||
[], | ||
(ctx) => ctx.distanceFromCenter() | ||
], | ||
'heatmap-density': [ | ||
@@ -206,0 +216,0 @@ NumberType, |
// @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'; | ||
@@ -17,2 +20,4 @@ const geometryTypes = ['Unknown', 'Point', 'LineString', 'Polygon']; | ||
canonical: ?CanonicalTileID; | ||
featureTileCoord: ?Point; | ||
featureDistanceData: ?FeatureDistanceData; | ||
@@ -29,2 +34,4 @@ _parseColorCache: {[_: string]: ?Color}; | ||
this.canonical = null; | ||
this.featureTileCoord = null; | ||
this.featureDistanceData = null; | ||
} | ||
@@ -52,2 +59,25 @@ | ||
distanceFromCenter() { | ||
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 { | ||
@@ -54,0 +84,0 @@ let cached = this._parseColorCache[input]; |
@@ -30,2 +30,3 @@ // @flow | ||
import type {CanonicalTileID} from '../../source/tile_id.js'; | ||
import type {FeatureDistanceData} from '../feature_filter/index.js'; | ||
@@ -44,2 +45,3 @@ export type Feature = { | ||
zoom: number, | ||
pitch?: number, | ||
heatmapDensity?: number, | ||
@@ -68,3 +70,3 @@ lineProgress?: number, | ||
evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection): any { | ||
evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection, featureTileCoord?: Point, featureDistanceData?: FeatureDistanceData): any { | ||
this._evaluator.globals = globals; | ||
@@ -76,2 +78,4 @@ this._evaluator.feature = feature; | ||
this._evaluator.formattedSection = formattedSection; | ||
this._evaluator.featureTileCoord = featureTileCoord || null; | ||
this._evaluator.featureDistanceData = featureDistanceData || null; | ||
@@ -81,3 +85,3 @@ return this.expression.evaluate(this._evaluator); | ||
evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection): any { | ||
evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection, featureTileCoord?: Point, featureDistanceData?: FeatureDistanceData): any { | ||
this._evaluator.globals = globals; | ||
@@ -89,2 +93,4 @@ this._evaluator.feature = feature || null; | ||
this._evaluator.formattedSection = formattedSection || null; | ||
this._evaluator.featureTileCoord = featureTileCoord || null; | ||
this._evaluator.featureDistanceData = featureDistanceData || null; | ||
@@ -242,3 +248,3 @@ try { | ||
const isZoomConstant = isConstant.isGlobalPropertyConstant(parsed, ['zoom']); | ||
const isZoomConstant = isConstant.isGlobalPropertyConstant(parsed, ['zoom', 'pitch', 'distance-from-center']); | ||
if (!isZoomConstant && !supportsZoomExpression(propertySpec)) { | ||
@@ -245,0 +251,0 @@ return error([new ParsingError('', 'zoom expressions not supported')]); |
@@ -232,3 +232,3 @@ // @flow | ||
return isFeatureConstant(expression) && | ||
isGlobalPropertyConstant(expression, ['zoom', 'heatmap-density', 'line-progress', 'sky-radial-progress', 'accumulated', 'is-supported-script']); | ||
isGlobalPropertyConstant(expression, ['zoom', 'heatmap-density', 'line-progress', 'sky-radial-progress', 'accumulated', 'is-supported-script', 'pitch', 'distance-from-center']); | ||
} |
// @flow | ||
import {createExpression} from '../expression/index.js'; | ||
import {isFeatureConstant} from '../expression/is_constant.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'; | ||
type FilterExpression = (globalProperties: GlobalProperties, feature: Feature, canonical?: CanonicalTileID) => boolean; | ||
export type FeatureFilter ={filter: FilterExpression, needGeometry: boolean}; | ||
export type FeatureDistanceData = {bearing: [number, number], center: [number, number], scale: number}; | ||
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}; | ||
export {isExpressionFilter, isDynamicFilter, extractStaticFilter}; | ||
@@ -55,13 +59,2 @@ function isExpressionFilter(filter: any) { | ||
const filterSpec = { | ||
'type': 'boolean', | ||
'default': false, | ||
'transition': false, | ||
'property-type': 'data-driven', | ||
'expression': { | ||
'interpolated': false, | ||
'parameters': ['zoom', 'feature'] | ||
} | ||
}; | ||
/** | ||
@@ -74,7 +67,8 @@ * Given a filter expressed as nested arrays, return a new function | ||
* @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): FeatureFilter { | ||
function createFilter(filter: any, layerType?: string = 'fill'): FeatureFilter { | ||
if (filter === null || filter === undefined) { | ||
return {filter: () => true, needGeometry: false}; | ||
return {filter: () => true, needGeometry: false, needFeature: false}; | ||
} | ||
@@ -85,13 +79,179 @@ | ||
} | ||
const filterExp = ((filter: any): string[] | string | boolean); | ||
const compiled = createExpression(filter, filterSpec); | ||
if (compiled.result === 'error') { | ||
throw new Error(compiled.value.map(err => `${err.key}: ${err.message}`).join(', ')); | ||
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 { | ||
const needGeometry = geometryNeeded(filter); | ||
return {filter: (globalProperties: GlobalProperties, feature: Feature, canonical?: CanonicalTileID) => compiled.value.evaluate(globalProperties, feature, {}, canonical), | ||
needGeometry}; | ||
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 = filter.slice(); | ||
// 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 | ||
@@ -98,0 +258,0 @@ function compare(a, b) { |
{ | ||
"name": "@mapbox/mapbox-gl-style-spec", | ||
"description": "a specification for mapbox gl styles", | ||
"version": "13.22.0", | ||
"version": "13.23.0-beta.1", | ||
"author": "Mapbox", | ||
@@ -6,0 +6,0 @@ "keywords": [ |
// @flow | ||
type ExpressionType = 'data-driven' | 'cross-faded' | 'cross-faded-data-driven' | 'color-ramp' | 'data-constant' | 'constant'; | ||
type ExpressionParameters = Array<'zoom' | 'feature' | 'feature-state' | 'heatmap-density' | 'line-progress' | 'sky-radial-progress'>; | ||
type ExpressionParameters = Array<'zoom' | 'feature' | 'feature-state' | 'heatmap-density' | 'line-progress' | 'sky-radial-progress' | 'pitch' | 'distance-from-center'>; | ||
@@ -6,0 +6,0 @@ type ExpressionSpecification = { |
@@ -75,2 +75,3 @@ // @flow | ||
"transition"?: TransitionSpecification, | ||
"projection"?: ProjectionSpecification, | ||
"layers": Array<LayerSpecification> | ||
@@ -97,2 +98,8 @@ |} | ||
export type ProjectionSpecification = {| | ||
"name": "albers" | "equalEarth" | "equirectangular" | "lambertConformalConic" | "mercator" | "naturalEarth" | "winkelTripel", | ||
"center"?: [number, number], | ||
"parallels"?: [number, number] | ||
|} | ||
export type VectorSourceSpecification = { | ||
@@ -99,0 +106,0 @@ "type": "vector", |
@@ -8,3 +8,6 @@ // @flow | ||
import {isStateConstant, isGlobalPropertyConstant, isFeatureConstant} from '../expression/is_constant.js'; | ||
import CompoundExpression from '../expression/compound_expression.js'; | ||
import type {Expression} from '../expression/expression.js'; | ||
export default function validateExpression(options: any): Array<ValidationError> { | ||
@@ -30,4 +33,4 @@ const expression = (options.expressionContext === 'property' ? createPropertyExpression : createExpression)(deepUnbundle(options.value), options.valueSpec); | ||
if (options.expressionContext === 'filter' && !isStateConstant(expressionObj)) { | ||
return [new ValidationError(options.key, options.value, '"feature-state" data expressions are not supported with filters.')]; | ||
if (options.expressionContext === 'filter') { | ||
return disallowedFilterParameters(expressionObj, options); | ||
} | ||
@@ -46,1 +49,29 @@ | ||
} | ||
export function disallowedFilterParameters(e: Expression, options: any): Array<ValidationError> { | ||
const disallowedParameters = new Set([ | ||
'zoom', | ||
'feature-state', | ||
'pitch', | ||
'distance-from-center' | ||
]); | ||
for (const param of options.valueSpec.expression.parameters) { | ||
disallowedParameters.delete(param); | ||
} | ||
if (disallowedParameters.size === 0) { | ||
return []; | ||
} | ||
const errors = []; | ||
if (e instanceof CompoundExpression) { | ||
if (disallowedParameters.has(e.name)) { | ||
return [new ValidationError(options.key, options.value, `["${e.name}"] expression is not supported in a filter for a ${options.object.type} layer with id: ${options.object.id}`)]; | ||
} | ||
} | ||
e.eachChild((arg) => { | ||
errors.push(...disallowedFilterParameters(arg, options)); | ||
}); | ||
return errors; | ||
} |
@@ -12,5 +12,7 @@ | ||
if (isExpressionFilter(deepUnbundle(options.value))) { | ||
const layerType = deepUnbundle(options.layerType); | ||
return validateExpression(extend({}, options, { | ||
expressionContext: 'filter', | ||
valueSpec: {value: 'boolean'} | ||
// We default to a layerType of `fill` because that points to a non-dynamic filter definition within the style-spec. | ||
valueSpec: options.styleSpec[`filter_${layerType || 'fill'}`] | ||
})); | ||
@@ -17,0 +19,0 @@ } else { |
@@ -101,3 +101,5 @@ | ||
}, | ||
filter: validateFilter, | ||
filter(options) { | ||
return validateFilter(extend({layerType: type}, options)); | ||
}, | ||
layout(options) { | ||
@@ -104,0 +106,0 @@ return validateObject({ |
@@ -25,2 +25,3 @@ | ||
import validateImage from './validate_image.js'; | ||
import validateProjection from './validate_projection.js'; | ||
@@ -47,3 +48,4 @@ const VALIDATORS = { | ||
'formatted': validateFormatted, | ||
'resolvedImage': validateImage | ||
'resolvedImage': validateImage, | ||
'projection': validateProjection | ||
}; | ||
@@ -50,0 +52,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
3042531
122
45321
2