diff --git a/bench/benchmarks/symbol_layout.js b/bench/benchmarks/symbol_layout.js index 98f26854313..69397653456 100644 --- a/bench/benchmarks/symbol_layout.js +++ b/bench/benchmarks/symbol_layout.js @@ -37,7 +37,8 @@ export default class SymbolLayout extends Layout { tileResult.glyphPositions, tileResult.iconMap, tileResult.imageAtlas.iconPositions, - false); + false, + tileResult.tileID.canonical); } } }); diff --git a/src/data/bucket.js b/src/data/bucket.js index f93ba793a84..5bbe276d4b2 100644 --- a/src/data/bucket.js +++ b/src/data/bucket.js @@ -7,6 +7,7 @@ import type FeatureIndex from './feature_index'; import type Context from '../gl/context'; import type {FeatureStates} from '../source/source_state'; import type {ImagePosition} from '../render/image_atlas'; +import type {CanonicalTileID} from '../source/tile_id'; export type BucketParameters = { index: number, @@ -74,7 +75,7 @@ export interface Bucket { +layers: Array; +stateDependentLayers: Array; +stateDependentLayerIds: Array; - populate(features: Array, options: PopulateParameters): void; + populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID): void; update(states: FeatureStates, vtLayer: VectorTileLayer, imagePositions: {[_: string]: ImagePosition}): void; isEmpty(): boolean; diff --git a/src/data/bucket/circle_bucket.js b/src/data/bucket/circle_bucket.js index 5e2a37ec1d1..e9f86df667c 100644 --- a/src/data/bucket/circle_bucket.js +++ b/src/data/bucket/circle_bucket.js @@ -11,6 +11,7 @@ import EXTENT from '../extent'; import {register} from '../../util/web_worker_transfer'; import EvaluationParameters from '../../style/evaluation_parameters'; +import type {CanonicalTileID} from '../../source/tile_id'; import type { Bucket, BucketParameters, @@ -75,7 +76,7 @@ class CircleBucket implements Bucke this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id); } - populate(features: Array, options: PopulateParameters) { + populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID) { const styleLayer = this.layers[0]; const bucketFeatures = []; let circleSortKey = null; @@ -86,25 +87,32 @@ class CircleBucket implements Bucke } for (const {feature, id, index, sourceLayerIndex} of features) { - if (this.layers[0]._featureFilter(new EvaluationParameters(this.zoom), feature)) { - const geometry = loadGeometry(feature); - const sortKey = circleSortKey ? - circleSortKey.evaluate(feature, {}) : - undefined; - - const bucketFeature: BucketFeature = { - id, - properties: feature.properties, - type: feature.type, - sourceLayerIndex, - index, - geometry, - patterns: {}, - sortKey - }; - - bucketFeatures.push(bucketFeature); - } + const needGeometry = this.layers[0]._featureFilter.needGeometry; + const evaluationFeature = {type: feature.type, + id, + properties: feature.properties, + geometry: needGeometry ? loadGeometry(feature) : []}; + + if (!this.layers[0]._featureFilter.filter(new EvaluationParameters(this.zoom), evaluationFeature, canonical)) continue; + + if (!needGeometry) evaluationFeature.geometry = loadGeometry(feature); + const sortKey = circleSortKey ? + circleSortKey.evaluate(evaluationFeature, {}, canonical) : + undefined; + + const bucketFeature: BucketFeature = { + id, + properties: feature.properties, + type: feature.type, + sourceLayerIndex, + index, + geometry : evaluationFeature.geometry, + patterns: {}, + sortKey + }; + + bucketFeatures.push(bucketFeature); + } if (circleSortKey) { @@ -118,7 +126,7 @@ class CircleBucket implements Bucke const {geometry, index, sourceLayerIndex} = bucketFeature; const feature = features[index].feature; - this.addFeature(bucketFeature, geometry, index); + this.addFeature(bucketFeature, geometry, index, canonical); options.featureIndex.insert(feature, geometry, index, sourceLayerIndex, this.index); } } @@ -153,7 +161,7 @@ class CircleBucket implements Bucke this.segments.destroy(); } - addFeature(feature: BucketFeature, geometry: Array>, index: number) { + addFeature(feature: BucketFeature, geometry: Array>, index: number, canonical: CanonicalTileID) { for (const ring of geometry) { for (const point of ring) { const x = point.x; @@ -187,7 +195,7 @@ class CircleBucket implements Bucke } } - this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, {}); + this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, {}, canonical); } } diff --git a/src/data/bucket/fill_bucket.js b/src/data/bucket/fill_bucket.js index 48eac842225..297ee0a872c 100644 --- a/src/data/bucket/fill_bucket.js +++ b/src/data/bucket/fill_bucket.js @@ -15,6 +15,7 @@ import {hasPattern, addPatternDependencies} from './pattern_bucket_features'; import loadGeometry from '../load_geometry'; import EvaluationParameters from '../../style/evaluation_parameters'; +import type {CanonicalTileID} from '../../source/tile_id'; import type { Bucket, BucketParameters, @@ -73,17 +74,24 @@ class FillBucket implements Bucket { this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id); } - populate(features: Array, options: PopulateParameters) { + populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID) { this.hasPattern = hasPattern('fill', this.layers, options); const fillSortKey = this.layers[0].layout.get('fill-sort-key'); const bucketFeatures = []; for (const {feature, id, index, sourceLayerIndex} of features) { - if (!this.layers[0]._featureFilter(new EvaluationParameters(this.zoom), feature)) continue; + const needGeometry = this.layers[0]._featureFilter.needGeometry; + const evaluationFeature = {type: feature.type, + id, + properties: feature.properties, + geometry: needGeometry ? loadGeometry(feature) : []}; + + if (!this.layers[0]._featureFilter.filter(new EvaluationParameters(this.zoom), evaluationFeature, canonical)) continue; + + if (!needGeometry) evaluationFeature.geometry = loadGeometry(feature); - const geometry = loadGeometry(feature); const sortKey = fillSortKey ? - fillSortKey.evaluate(feature, {}, options.availableImages) : + fillSortKey.evaluate(evaluationFeature, {}, canonical, options.availableImages) : undefined; const bucketFeature: BucketFeature = { @@ -92,7 +100,7 @@ class FillBucket implements Bucket { type: feature.type, sourceLayerIndex, index, - geometry, + geometry: evaluationFeature.geometry, patterns: {}, sortKey }; @@ -116,7 +124,7 @@ class FillBucket implements Bucket { // so are stored during populate until later updated with positions by tile worker in addFeatures this.patternFeatures.push(patternFeature); } else { - this.addFeature(bucketFeature, geometry, index, {}); + this.addFeature(bucketFeature, geometry, index, canonical, {}); } const feature = features[index].feature; @@ -129,9 +137,9 @@ class FillBucket implements Bucket { this.programConfigurations.updatePaintArrays(states, vtLayer, this.stateDependentLayers, imagePositions); } - addFeatures(options: PopulateParameters, imagePositions: {[_: string]: ImagePosition}) { + addFeatures(options: PopulateParameters, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) { for (const feature of this.patternFeatures) { - this.addFeature(feature, feature.geometry, feature.index, imagePositions); + this.addFeature(feature, feature.geometry, feature.index, canonical, imagePositions); } } @@ -162,7 +170,7 @@ class FillBucket implements Bucket { this.segments2.destroy(); } - addFeature(feature: BucketFeature, geometry: Array>, index: number, imagePositions: {[_: string]: ImagePosition}) { + addFeature(feature: BucketFeature, geometry: Array>, index: number, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) { for (const polygon of classifyRings(geometry, EARCUT_MAX_RINGS)) { let numVertices = 0; for (const ring of polygon) { @@ -216,7 +224,7 @@ class FillBucket implements Bucket { triangleSegment.vertexLength += numVertices; triangleSegment.primitiveLength += indices.length / 3; } - this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, imagePositions); + this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, imagePositions, canonical); } } diff --git a/src/data/bucket/fill_extrusion_bucket.js b/src/data/bucket/fill_extrusion_bucket.js index 1f98d3d5dfc..bf507349946 100644 --- a/src/data/bucket/fill_extrusion_bucket.js +++ b/src/data/bucket/fill_extrusion_bucket.js @@ -18,6 +18,7 @@ import {hasPattern, addPatternDependencies} from './pattern_bucket_features'; import loadGeometry from '../load_geometry'; import EvaluationParameters from '../../style/evaluation_parameters'; +import type {CanonicalTileID} from '../../source/tile_id'; import type { Bucket, BucketParameters, @@ -87,20 +88,24 @@ class FillExtrusionBucket implements Bucket { } - populate(features: Array, options: PopulateParameters) { + populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID) { this.features = []; this.hasPattern = hasPattern('fill-extrusion', this.layers, options); for (const {feature, id, index, sourceLayerIndex} of features) { - if (!this.layers[0]._featureFilter(new EvaluationParameters(this.zoom), feature)) continue; + const needGeometry = this.layers[0]._featureFilter.needGeometry; + const evaluationFeature = {type: feature.type, + id, + properties: feature.properties, + geometry: needGeometry ? loadGeometry(feature) : []}; - const geometry = loadGeometry(feature); + if (!this.layers[0]._featureFilter.filter(new EvaluationParameters(this.zoom), evaluationFeature, canonical)) continue; const patternFeature: BucketFeature = { id, sourceLayerIndex, index, - geometry, + geometry: needGeometry ? evaluationFeature.geometry : loadGeometry(feature), properties: feature.properties, type: feature.type, patterns: {} @@ -113,17 +118,17 @@ class FillExtrusionBucket implements Bucket { if (this.hasPattern) { this.features.push(addPatternDependencies('fill-extrusion', this.layers, patternFeature, this.zoom, options)); } else { - this.addFeature(patternFeature, geometry, index, {}); + this.addFeature(patternFeature, patternFeature.geometry, index, canonical, {}); } - options.featureIndex.insert(feature, geometry, index, sourceLayerIndex, this.index, true); + options.featureIndex.insert(feature, patternFeature.geometry, index, sourceLayerIndex, this.index, true); } } - addFeatures(options: PopulateParameters, imagePositions: {[_: string]: ImagePosition}) { + addFeatures(options: PopulateParameters, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) { for (const feature of this.features) { const {geometry} = feature; - this.addFeature(feature, geometry, feature.index, imagePositions); + this.addFeature(feature, geometry, feature.index, canonical, imagePositions); } } @@ -157,7 +162,7 @@ class FillExtrusionBucket implements Bucket { this.segments.destroy(); } - addFeature(feature: BucketFeature, geometry: Array>, index: number, imagePositions: {[_: string]: ImagePosition}) { + addFeature(feature: BucketFeature, geometry: Array>, index: number, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) { for (const polygon of classifyRings(geometry, EARCUT_MAX_RINGS)) { let numVertices = 0; for (const ring of polygon) { @@ -263,7 +268,7 @@ class FillExtrusionBucket implements Bucket { segment.vertexLength += numVertices; } - this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, imagePositions); + this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, imagePositions, canonical); } } diff --git a/src/data/bucket/line_bucket.js b/src/data/bucket/line_bucket.js index b68c54dbe9d..db661fa67a3 100644 --- a/src/data/bucket/line_bucket.js +++ b/src/data/bucket/line_bucket.js @@ -14,6 +14,7 @@ import {hasPattern, addPatternDependencies} from './pattern_bucket_features'; import loadGeometry from '../load_geometry'; import EvaluationParameters from '../../style/evaluation_parameters'; +import type {CanonicalTileID} from '../../source/tile_id'; import type { Bucket, BucketParameters, @@ -116,17 +117,24 @@ class LineBucket implements Bucket { this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id); } - populate(features: Array, options: PopulateParameters) { + populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID) { this.hasPattern = hasPattern('line', this.layers, options); const lineSortKey = this.layers[0].layout.get('line-sort-key'); const bucketFeatures = []; for (const {feature, id, index, sourceLayerIndex} of features) { - if (!this.layers[0]._featureFilter(new EvaluationParameters(this.zoom), feature)) continue; + const needGeometry = this.layers[0]._featureFilter.needGeometry; + const evaluationFeature = {type: feature.type, + id, + properties: feature.properties, + geometry: needGeometry ? loadGeometry(feature) : []}; + + if (!this.layers[0]._featureFilter.filter(new EvaluationParameters(this.zoom), evaluationFeature, canonical)) continue; + + if (!needGeometry) evaluationFeature.geometry = loadGeometry(feature); - const geometry = loadGeometry(feature); const sortKey = lineSortKey ? - lineSortKey.evaluate(feature, {}) : + lineSortKey.evaluate(evaluationFeature, {}, canonical) : undefined; const bucketFeature: BucketFeature = { @@ -135,7 +143,7 @@ class LineBucket implements Bucket { type: feature.type, sourceLayerIndex, index, - geometry, + geometry: evaluationFeature.geometry, patterns: {}, sortKey }; @@ -159,7 +167,7 @@ class LineBucket implements Bucket { // so are stored during populate until later updated with positions by tile worker in addFeatures this.patternFeatures.push(patternBucketFeature); } else { - this.addFeature(bucketFeature, geometry, index, {}); + this.addFeature(bucketFeature, geometry, index, canonical, {}); } const feature = features[index].feature; @@ -172,9 +180,9 @@ class LineBucket implements Bucket { this.programConfigurations.updatePaintArrays(states, vtLayer, this.stateDependentLayers, imagePositions); } - addFeatures(options: PopulateParameters, imagePositions: {[_: string]: ImagePosition}) { + addFeatures(options: PopulateParameters, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) { for (const feature of this.patternFeatures) { - this.addFeature(feature, feature.geometry, feature.index, imagePositions); + this.addFeature(feature, feature.geometry, feature.index, canonical, imagePositions); } } @@ -203,7 +211,7 @@ class LineBucket implements Bucket { this.segments.destroy(); } - addFeature(feature: BucketFeature, geometry: Array>, index: number, imagePositions: {[_: string]: ImagePosition}) { + addFeature(feature: BucketFeature, geometry: Array>, index: number, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) { const layout = this.layers[0].layout; const join = layout.get('line-join').evaluate(feature, {}); const cap = layout.get('line-cap'); @@ -211,11 +219,11 @@ class LineBucket implements Bucket { const roundLimit = layout.get('line-round-limit'); for (const line of geometry) { - this.addLine(line, feature, join, cap, miterLimit, roundLimit, index, imagePositions); + this.addLine(line, feature, join, cap, miterLimit, roundLimit, index, canonical, imagePositions); } } - addLine(vertices: Array, feature: BucketFeature, join: string, cap: string, miterLimit: number, roundLimit: number, index: number, imagePositions: {[_: string]: ImagePosition}) { + addLine(vertices: Array, feature: BucketFeature, join: string, cap: string, miterLimit: number, roundLimit: number, index: number, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) { this.distance = 0; this.scaledDistance = 0; this.totalDistance = 0; @@ -461,7 +469,7 @@ class LineBucket implements Bucket { } } - this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, imagePositions); + this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, imagePositions, canonical); } /** diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index ceb2b6e92a1..2e57143c896 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -40,6 +40,7 @@ import Formatted from '../../style-spec/expression/types/formatted'; import ResolvedImage from '../../style-spec/expression/types/resolved_image'; import {plugin as globalRTLTextPlugin, getRTLTextPluginStatus} from '../../source/rtl_text_plugin'; +import type {CanonicalTileID} from '../../source/tile_id'; import type { Bucket, BucketParameters, @@ -399,7 +400,7 @@ class SymbolBucket implements Bucket { } } - populate(features: Array, options: PopulateParameters) { + populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID) { const layer = this.layers[0]; const layout = layer.layout; @@ -430,16 +431,25 @@ class SymbolBucket implements Bucket { const globalProperties = new EvaluationParameters(this.zoom); for (const {feature, id, index, sourceLayerIndex} of features) { - if (!layer._featureFilter(globalProperties, feature)) { + + const needGeometry = layer._featureFilter.needGeometry; + const evaluationFeature = {type: feature.type, + id, + properties: feature.properties, + geometry: needGeometry ? loadGeometry(feature) : []}; + + if (!layer._featureFilter.filter(globalProperties, evaluationFeature, canonical)) { continue; } + if (!needGeometry) evaluationFeature.geometry = loadGeometry(feature); + let text: Formatted | void; if (hasText) { // Expression evaluation will automatically coerce to Formatted // but plain string token evaluation skips that pathway so do the // conversion here. - const resolvedTokens = layer.getValueAndResolveTokens('text-field', feature, availableImages); + const resolvedTokens = layer.getValueAndResolveTokens('text-field', evaluationFeature, canonical, availableImages); const formattedText = Formatted.factory(resolvedTokens); if (containsRTLText(formattedText)) { this.hasRTLText = true; @@ -449,7 +459,7 @@ class SymbolBucket implements Bucket { getRTLTextPluginStatus() === 'unavailable' || // We don't intend to lazy-load the rtl text plugin, so proceed with incorrect shaping this.hasRTLText && globalRTLTextPlugin.isParsed() // Use the rtlText plugin to shape text ) { - text = transformText(formattedText, layer, feature); + text = transformText(formattedText, layer, evaluationFeature); } } @@ -458,7 +468,7 @@ class SymbolBucket implements Bucket { // Expression evaluation will automatically coerce to Image // but plain string token evaluation skips that pathway so do the // conversion here. - const resolvedTokens = layer.getValueAndResolveTokens('icon-image', feature, availableImages); + const resolvedTokens = layer.getValueAndResolveTokens('icon-image', evaluationFeature, canonical, availableImages); if (resolvedTokens instanceof ResolvedImage) { icon = resolvedTokens; } else { @@ -469,9 +479,8 @@ class SymbolBucket implements Bucket { if (!text && !icon) { continue; } - const sortKey = this.sortFeaturesByKey ? - symbolSortKey.evaluate(feature, {}) : + symbolSortKey.evaluate(evaluationFeature, {}, canonical) : undefined; const symbolFeature: SymbolFeature = { @@ -492,7 +501,7 @@ class SymbolBucket implements Bucket { } if (text) { - const fontStack = textFont.evaluate(feature, {}).join(','); + const fontStack = textFont.evaluate(evaluationFeature, {}, canonical).join(','); const textAlongLine = layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') !== 'point'; this.allowVerticalPlacement = this.writingModes && this.writingModes.indexOf(WritingMode.vertical) >= 0; for (const section of text.sections) { @@ -606,7 +615,8 @@ class SymbolBucket implements Bucket { labelAnchor: Anchor, lineStartIndex: number, lineLength: number, - associatedIconIndex: number) { + associatedIconIndex: number, + canonical: CanonicalTileID) { const indexArray = arrays.indexArray; const layoutVertexArray = arrays.layoutVertexArray; @@ -639,7 +649,7 @@ class SymbolBucket implements Bucket { this.glyphOffsetArray.emplaceBack(glyphOffset[0]); if (i === quads.length - 1 || sectionIndex !== quads[i + 1].sectionIndex) { - arrays.programConfigurations.populatePaintArrays(layoutVertexArray.length, feature, feature.index, {}, sections && sections[sectionIndex]); + arrays.programConfigurations.populatePaintArrays(layoutVertexArray.length, feature, feature.index, {}, canonical, sections && sections[sectionIndex]); } } diff --git a/src/data/feature_index.js b/src/data/feature_index.js index 2937d314d3c..ae2eca0f595 100644 --- a/src/data/feature_index.js +++ b/src/data/feature_index.js @@ -185,7 +185,7 @@ class FeatureIndex { const sourceLayer = this.vtLayers[sourceLayerName]; const feature = sourceLayer.feature(featureIndex); - if (!filter(new EvaluationParameters(this.tileID.overscaledZ), feature)) + if (!filter.filter(new EvaluationParameters(this.tileID.overscaledZ), feature)) return; const id = this.getId(feature, sourceLayerName); diff --git a/src/data/program_configuration.js b/src/data/program_configuration.js index 22e9cba0d3f..febd4779f2d 100644 --- a/src/data/program_configuration.js +++ b/src/data/program_configuration.js @@ -18,6 +18,7 @@ import { type UniformLocations } from '../render/uniform_binding'; +import type {CanonicalTileID} from '../source/tile_id'; import type Context from '../gl/context'; import type {TypedStyleLayer} from '../style/style_layer/typed_style_layer'; import type {CrossfadeParameters} from '../style/evaluation_parameters'; @@ -77,7 +78,7 @@ function packColor(color: Color): [number, number] { */ interface AttributeBinder { - populatePaintArray(length: number, feature: Feature, imagePositions: {[_: string]: ImagePosition}, formattedSection?: FormattedSection): void; + populatePaintArray(length: number, feature: Feature, imagePositions: {[_: string]: ImagePosition}, canonical?: CanonicalTileID, formattedSection?: FormattedSection): void; updatePaintArray(start: number, length: number, feature: Feature, featureState: FeatureState, imagePositions: {[_: string]: ImagePosition}): void; upload(Context): void; destroy(): void; @@ -161,9 +162,9 @@ class SourceExpressionBinder implements AttributeBinder { this.paintVertexArray = new PaintVertexArray(); } - populatePaintArray(newLength: number, feature: Feature, imagePositions: {[_: string]: ImagePosition}, formattedSection?: FormattedSection) { + populatePaintArray(newLength: number, feature: Feature, imagePositions: {[_: string]: ImagePosition}, canonical?: CanonicalTileID, formattedSection?: FormattedSection) { const start = this.paintVertexArray.length; - const value = this.expression.evaluate(new EvaluationParameters(0), feature, {}, [], formattedSection); + const value = this.expression.evaluate(new EvaluationParameters(0), feature, {}, canonical, [], formattedSection); this.paintVertexArray.resize(newLength); this._setPaintValue(start, newLength, value); } @@ -232,9 +233,9 @@ class CompositeExpressionBinder implements AttributeBinder, UniformBinder { this.paintVertexArray = new PaintVertexArray(); } - populatePaintArray(newLength: number, feature: Feature, imagePositions: {[_: string]: ImagePosition}, formattedSection?: FormattedSection) { - const min = this.expression.evaluate(new EvaluationParameters(this.zoom), feature, {}, [], formattedSection); - const max = this.expression.evaluate(new EvaluationParameters(this.zoom + 1), feature, {}, [], formattedSection); + populatePaintArray(newLength: number, feature: Feature, imagePositions: {[_: string]: ImagePosition}, canonical?: CanonicalTileID, formattedSection?: FormattedSection) { + const min = this.expression.evaluate(new EvaluationParameters(this.zoom), feature, {}, canonical, [], formattedSection); + const max = this.expression.evaluate(new EvaluationParameters(this.zoom + 1), feature, {}, canonical, [], formattedSection); const start = this.paintVertexArray.length; this.paintVertexArray.resize(newLength); this._setPaintValue(start, newLength, min, max); @@ -442,11 +443,11 @@ export default class ProgramConfiguration { return binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder ? binder.maxValue : 0; } - populatePaintArrays(newLength: number, feature: Feature, imagePositions: {[_: string]: ImagePosition}, formattedSection?: FormattedSection) { + populatePaintArrays(newLength: number, feature: Feature, imagePositions: {[_: string]: ImagePosition}, canonical?: CanonicalTileID, formattedSection?: FormattedSection) { for (const property in this.binders) { const binder = this.binders[property]; if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof CrossFadedCompositeBinder) - (binder: AttributeBinder).populatePaintArray(newLength, feature, imagePositions, formattedSection); + (binder: AttributeBinder).populatePaintArray(newLength, feature, imagePositions, canonical, formattedSection); } } setConstantPatternPositions(posTo: ImagePosition, posFrom: ImagePosition) { @@ -569,9 +570,9 @@ export class ProgramConfigurationSet { this._bufferOffset = 0; } - populatePaintArrays(length: number, feature: Feature, index: number, imagePositions: {[_: string]: ImagePosition}, formattedSection?: FormattedSection) { + populatePaintArrays(length: number, feature: Feature, index: number, imagePositions: {[_: string]: ImagePosition}, canonical: CanonicalTileID, formattedSection?: FormattedSection) { for (const key in this.programConfigurations) { - this.programConfigurations[key].populatePaintArrays(length, feature, imagePositions, formattedSection); + this.programConfigurations[key].populatePaintArrays(length, feature, imagePositions, canonical, formattedSection); } if (feature.id !== undefined) { diff --git a/src/source/tile.js b/src/source/tile.js index d9bd4b51889..1b20598212e 100644 --- a/src/source/tile.js +++ b/src/source/tile.js @@ -307,7 +307,7 @@ class Tile { for (let i = 0; i < layer.length; i++) { const feature = layer.feature(i); - if (filter(new EvaluationParameters(this.tileID.overscaledZ), feature)) { + if (filter.filter(new EvaluationParameters(this.tileID.overscaledZ), feature)) { const id = featureIndex.getId(feature, sourceLayer); const geojsonFeature = new GeoJSONFeature(feature, z, x, y, id); (geojsonFeature: any).tile = coord; diff --git a/src/source/worker_tile.js b/src/source/worker_tile.js index 4a49625997b..fd5e8d5d502 100644 --- a/src/source/worker_tile.js +++ b/src/source/worker_tile.js @@ -124,7 +124,7 @@ class WorkerTile { sourceID: this.source }); - bucket.populate(features, options); + bucket.populate(features, options, this.tileID.canonical); featureIndex.bucketLayerIDs.push(family.map((l) => l.id)); } } @@ -186,13 +186,13 @@ class WorkerTile { const bucket = buckets[key]; if (bucket instanceof SymbolBucket) { recalculateLayers(bucket.layers, this.zoom, availableImages); - performSymbolLayout(bucket, glyphMap, glyphAtlas.positions, iconMap, imageAtlas.iconPositions, this.showCollisionBoxes); + performSymbolLayout(bucket, glyphMap, glyphAtlas.positions, iconMap, imageAtlas.iconPositions, this.showCollisionBoxes, this.tileID.canonical); } else if (bucket.hasPattern && (bucket instanceof LineBucket || bucket instanceof FillBucket || bucket instanceof FillExtrusionBucket)) { recalculateLayers(bucket.layers, this.zoom, availableImages); - bucket.addFeatures(options, imageAtlas.patternPositions); + bucket.addFeatures(options, this.tileID.canonical, imageAtlas.patternPositions); } } diff --git a/src/style-spec/expression/definitions/index.js b/src/style-spec/expression/definitions/index.js index 7ec0107a549..3114f7c059c 100644 --- a/src/style-spec/expression/definitions/index.js +++ b/src/style-spec/expression/definitions/index.js @@ -42,6 +42,7 @@ import NumberFormat from './number_format'; import FormatExpression from './format'; import ImageExpression from './image'; import Length from './length'; +import Within from './within'; import type {Varargs} from '../compound_expression'; import type {ExpressionRegistry} from '../expression'; @@ -79,7 +80,8 @@ const expressions: ExpressionRegistry = { 'to-color': Coercion, 'to-number': Coercion, 'to-string': Coercion, - 'var': Var + 'var': Var, + 'within': Within }; function rgba(ctx, [r, g, b, a]) { @@ -455,7 +457,7 @@ CompoundExpression.register(expressions, { 'filter-has-id': [ BooleanType, [], - (ctx) => ctx.id() !== null + (ctx) => (ctx.id() !== null && ctx.id() !== undefined) ], 'filter-type-in': [ BooleanType, diff --git a/src/style-spec/expression/definitions/within.js b/src/style-spec/expression/definitions/within.js new file mode 100644 index 00000000000..0d7d14d452e --- /dev/null +++ b/src/style-spec/expression/definitions/within.js @@ -0,0 +1,273 @@ +// @flow + +import {isValue} from '../values'; +import type {Type} from '../types'; +import {BooleanType} from '../types'; +import type {Expression} from '../expression'; +import type ParsingContext from '../parsing_context'; +import type EvaluationContext from '../evaluation_context'; +import type {GeoJSON, GeoJSONPolygon, GeoJSONMultiPolygon} from '@mapbox/geojson-types'; +import MercatorCoordinate from '../../../geo/mercator_coordinate'; +import EXTENT from '../../../data/extent'; +import Point from '@mapbox/point-geometry'; + +type GeoJSONPolygons =| GeoJSONPolygon | GeoJSONMultiPolygon; + +type BBox = [number, number, number, number]; + +function calcBBox(bbox: BBox, geom, type) { + if (type === 'Point') { + updateBBox(bbox, geom); + } else if (type === 'MultiPoint' || type === 'LineString') { + for (let i = 0; i < geom.length; ++i) { + updateBBox(bbox, geom[i]); + } + } else if (type === 'Polygon' || type === 'MultiLineString') { + for (let i = 0; i < geom.length; i++) { + for (let j = 0; j < geom[i].length; j++) { + updateBBox(bbox, geom[i][j]); + } + } + } else if (type === 'MultiPolygon') { + for (let i = 0; i < geom.length; i++) { + for (let j = 0; j < geom[i].length; j++) { + for (let k = 0; k < geom[i][j].length; k++) { + updateBBox(bbox, geom[i][j][k]); + } + } + } + } +} + +function updateBBox(bbox: BBox, coord: Point) { + bbox[0] = Math.min(bbox[0], coord[0]); + bbox[1] = Math.min(bbox[1], coord[1]); + bbox[2] = Math.max(bbox[2], coord[0]); + bbox[3] = Math.max(bbox[3], coord[1]); +} + +function boxWithinBox(bbox1, bbox2) { + if (bbox1[0] <= bbox2[0]) return false; + if (bbox1[2] >= bbox2[2]) return false; + if (bbox1[1] <= bbox2[1]) return false; + if (bbox1[3] >= bbox2[3]) return false; + return true; +} + +function getLngLatPoint(coord: Point, canonical) { + const tilesAtZoom = Math.pow(2, canonical.z); + const x = (coord.x / EXTENT + canonical.x) / tilesAtZoom; + const y = (coord.y / EXTENT + canonical.y) / tilesAtZoom; + const p = new MercatorCoordinate(x, y).toLngLat(); + + return [p.lng, p.lat]; +} + +function getLngLatPoints(line, canonical) { + const coords = []; + for (let i = 0; i < line.length; ++i) { + coords.push(getLngLatPoint(line[i], canonical)); + } + return coords; +} + +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) { + if (polygons.type === 'Polygon') { + return pointWithinPolygon(point, polygons.coordinates); + } + for (let i = 0; i < polygons.coordinates.length; i++) { + if (!pointWithinPolygon(point, polygons.coordinates[i])) return false; + } + return true; +} + +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]; + if ((x1 * y3 - x3 * y1) * (x2 * y3 - x3 * y2) < 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) { + if (polygons.type === 'Polygon') { + return lineStringWithinPolygon(line, polygons.coordinates); + } + for (let i = 0; i < polygons.coordinates.length; i++) { + if (!lineStringWithinPolygon(line, polygons.coordinates[i])) return false; + } + return true; +} + +function pointsWithinPolygons(ctx: EvaluationContext, polygonGeometry: GeoJSONPolygons, polyBBox: BBox) { + const pointBBox = [Infinity, Infinity, -Infinity, -Infinity]; + const lngLatPoints = []; + for (const points of ctx.geometry()) { + for (const point of points) { + const p = getLngLatPoint(point, ctx.canonicalID()); + lngLatPoints.push(p); + updateBBox(pointBBox, p); + } + } + if (!boxWithinBox(pointBBox, polyBBox)) return false; + for (let i = 0; i < lngLatPoints.length; ++i) { + if (!pointWithinPolygons(lngLatPoints[i], polygonGeometry)) return false; + } + return true; +} + +function linesWithinPolygons(ctx: EvaluationContext, polygonGeometry: GeoJSONPolygons, polyBBox: BBox) { + const lineBBox = [Infinity, Infinity, -Infinity, -Infinity]; + const lineCoords = []; + for (const line of ctx.geometry()) { + const lineCoord = getLngLatPoints(line, ctx.canonicalID()); + lineCoords.push(lineCoord); + calcBBox(lineBBox, lineCoord, 'LineString'); + } + if (!boxWithinBox(lineBBox, polyBBox)) return false; + for (let i = 0; i < lineCoords.length; ++i) { + if (!lineStringWithinPolygons(lineCoords[i], polygonGeometry)) return false; + } + return true; +} + +class Within implements Expression { + type: Type; + geojson: GeoJSON + geometries: GeoJSONPolygons; + polyBBox: BBox; + + constructor(geojson: GeoJSON, geometries: GeoJSONPolygons) { + this.type = BooleanType; + this.geojson = geojson; + this.geometries = geometries; + this.polyBBox = [Infinity, Infinity, -Infinity, -Infinity]; + calcBBox(this.polyBBox, this.geometries.coordinates, this.geometries.type); + } + + static parse(args: $ReadOnlyArray, context: ParsingContext) { + 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) { + if (ctx.geometry() != null && ctx.canonicalID() != null) { + if (ctx.geometryType() === 'Point') { + return pointsWithinPolygons(ctx, this.geometries, this.polyBBox); + } else if (ctx.geometryType() === 'LineString') { + return linesWithinPolygons(ctx, this.geometries, this.polyBBox); + } + } + return false; + } + + eachChild() {} + + outputDefined(): boolean { + return true; + } + + serialize(): Array { + return ["within", this.geojson]; + } + +} + +export default Within; diff --git a/src/style-spec/expression/evaluation_context.js b/src/style-spec/expression/evaluation_context.js index 82c801b3725..1e7a6d587e1 100644 --- a/src/style-spec/expression/evaluation_context.js +++ b/src/style-spec/expression/evaluation_context.js @@ -3,6 +3,7 @@ import {Color} from './values'; import type {FormattedSection} from './types/formatted'; import type {GlobalProperties, Feature, FeatureState} from './index'; +import type {CanonicalTileID} from '../../source/tile_id'; const geometryTypes = ['Unknown', 'Point', 'LineString', 'Polygon']; @@ -12,6 +13,7 @@ class EvaluationContext { featureState: ?FeatureState; formattedSection: ?FormattedSection; availableImages: ?Array; + canonical: ?CanonicalTileID; _parseColorCache: {[_: string]: ?Color}; @@ -22,6 +24,7 @@ class EvaluationContext { this.formattedSection = null; this._parseColorCache = {}; this.availableImages = null; + this.canonical = null; } id() { @@ -32,6 +35,14 @@ class EvaluationContext { return this.feature ? typeof this.feature.type === 'number' ? geometryTypes[this.feature.type] : this.feature.type : null; } + geometry() { + return this.feature && 'geometry' in this.feature ? this.feature.geometry : null; + } + + canonicalID() { + return this.canonical; + } + properties() { return this.feature && this.feature.properties || {}; } diff --git a/src/style-spec/expression/index.js b/src/style-spec/expression/index.js index 077baa855d4..736ad8fdc21 100644 --- a/src/style-spec/expression/index.js +++ b/src/style-spec/expression/index.js @@ -25,12 +25,15 @@ import type {Result} from '../util/result'; import type {InterpolationType} from './definitions/interpolate'; import type {PropertyValueSpecification} from '../types'; import type {FormattedSection} from './types/formatted'; +import type Point from '@mapbox/point-geometry'; +import type {CanonicalTileID} from '../../source/tile_id'; export type Feature = { +type: 1 | 2 | 3 | 'Unknown' | 'Point' | 'MultiPoint' | 'LineString' | 'MultiLineString' | 'Polygon' | 'MultiPolygon', +id?: any, +properties: {[_: string]: any}, - +patterns?: {[_: string]: {"min": string, "mid": string, "max": string}} + +patterns?: {[_: string]: {"min": string, "mid": string, "max": string}}, + +geometry?: Array> }; export type FeatureState = {[_: string]: any}; @@ -59,20 +62,22 @@ export class StyleExpression { this._enumValues = propertySpec && propertySpec.type === 'enum' ? propertySpec.values : null; } - evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, availableImages?: Array, formattedSection?: FormattedSection): any { + evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection): any { this._evaluator.globals = globals; this._evaluator.feature = feature; this._evaluator.featureState = featureState; + this._evaluator.canonical = canonical; this._evaluator.availableImages = availableImages || null; this._evaluator.formattedSection = formattedSection; return this.expression.evaluate(this._evaluator); } - evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, availableImages?: Array, formattedSection?: FormattedSection): any { + evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection): any { this._evaluator.globals = globals; this._evaluator.feature = feature || null; this._evaluator.featureState = featureState || null; + this._evaluator.canonical = canonical; this._evaluator.availableImages = availableImages || null; this._evaluator.formattedSection = formattedSection || null; @@ -138,12 +143,12 @@ export class ZoomConstantExpression { this.isStateDependent = kind !== ('constant': EvaluationKind) && !isConstant.isStateConstant(expression.expression); } - evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, availableImages?: Array, formattedSection?: FormattedSection): any { - return this._styleExpression.evaluateWithoutErrorHandling(globals, feature, featureState, availableImages, formattedSection); + evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection): any { + return this._styleExpression.evaluateWithoutErrorHandling(globals, feature, featureState, canonical, availableImages, formattedSection); } - evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, availableImages?: Array, formattedSection?: FormattedSection): any { - return this._styleExpression.evaluate(globals, feature, featureState, availableImages, formattedSection); + evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection): any { + return this._styleExpression.evaluate(globals, feature, featureState, canonical, availableImages, formattedSection); } } @@ -163,12 +168,12 @@ export class ZoomDependentExpression { this.interpolationType = interpolationType; } - evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, availableImages?: Array, formattedSection?: FormattedSection): any { - return this._styleExpression.evaluateWithoutErrorHandling(globals, feature, featureState, availableImages, formattedSection); + evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection): any { + return this._styleExpression.evaluateWithoutErrorHandling(globals, feature, featureState, canonical, availableImages, formattedSection); } - evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, availableImages?: Array, formattedSection?: FormattedSection): any { - return this._styleExpression.evaluate(globals, feature, featureState, availableImages, formattedSection); + evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection): any { + return this._styleExpression.evaluate(globals, feature, featureState, canonical, availableImages, formattedSection); } interpolationFactor(input: number, lower: number, upper: number): number { @@ -182,18 +187,18 @@ export class ZoomDependentExpression { export type ConstantExpression = { kind: 'constant', - +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, availableImages?: Array) => any, + +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array) => any, } export type SourceExpression = { kind: 'source', isStateDependent: boolean, - +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, availableImages?: Array, formattedSection?: FormattedSection) => any, + +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection) => any, }; export type CameraExpression = { kind: 'camera', - +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, availableImages?: Array) => any, + +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array) => any, +interpolationFactor: (input: number, lower: number, upper: number) => number, zoomStops: Array, interpolationType: ?InterpolationType @@ -202,7 +207,7 @@ export type CameraExpression = { export type CompositeExpression = { kind: 'composite', isStateDependent: boolean, - +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, availableImages?: Array, formattedSection?: FormattedSection) => any, + +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection) => any, +interpolationFactor: (input: number, lower: number, upper: number) => number, zoomStops: Array, interpolationType: ?InterpolationType diff --git a/src/style-spec/expression/is_constant.js b/src/style-spec/expression/is_constant.js index 92e7ab24be2..bb997e899a5 100644 --- a/src/style-spec/expression/is_constant.js +++ b/src/style-spec/expression/is_constant.js @@ -1,7 +1,7 @@ // @flow import CompoundExpression from './compound_expression'; - +import Within from './definitions/within'; import type {Expression} from './expression.js'; function isFeatureConstant(e: Expression) { @@ -23,6 +23,10 @@ function isFeatureConstant(e: Expression) { } } + if (e instanceof Within) { + return false; + } + let result = true; e.eachChild(arg => { if (result && !isFeatureConstant(arg)) { result = false; } diff --git a/src/style-spec/expression/parsing_context.js b/src/style-spec/expression/parsing_context.js index e08fa11ff66..7cf8ce94e8c 100644 --- a/src/style-spec/expression/parsing_context.js +++ b/src/style-spec/expression/parsing_context.js @@ -9,6 +9,7 @@ import Coercion from './definitions/coercion'; import EvaluationContext from './evaluation_context'; import CompoundExpression from './compound_expression'; import CollatorExpression from './definitions/collator'; +import Within from './definitions/within'; import {isGlobalPropertyConstant, isFeatureConstant} from './is_constant'; import Var from './definitions/var'; @@ -201,6 +202,8 @@ function isConstant(expression: Expression) { // 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 || diff --git a/src/style-spec/feature_filter/index.js b/src/style-spec/feature_filter/index.js index b9c23e48ad6..ffe1662d3b5 100644 --- a/src/style-spec/feature_filter/index.js +++ b/src/style-spec/feature_filter/index.js @@ -1,9 +1,11 @@ // @flow import {createExpression} from '../expression'; +import type {GlobalProperties, Feature} from '../expression'; +import type {CanonicalTileID} from '../../source/tile_id'; -import type {GlobalProperties} from '../expression'; -export type FeatureFilter = (globalProperties: GlobalProperties, feature: VectorTileFeature) => boolean; +type FilterExpression = (globalProperties: GlobalProperties, feature: Feature, canonical?: CanonicalTileID) => boolean; +export type FeatureFilter ={filter: FilterExpression, needGeometry: boolean}; export default createFilter; export {isExpressionFilter}; @@ -72,7 +74,7 @@ const filterSpec = { */ function createFilter(filter: any): FeatureFilter { if (filter === null || filter === undefined) { - return () => true; + return {filter: () => true, needGeometry: false}; } if (!isExpressionFilter(filter)) { @@ -83,7 +85,9 @@ function createFilter(filter: any): FeatureFilter { if (compiled.result === 'error') { throw new Error(compiled.value.map(err => `${err.key}: ${err.message}`).join(', ')); } else { - return (globalProperties: GlobalProperties, feature: VectorTileFeature) => compiled.value.evaluate(globalProperties, feature); + const needGeometry = Array.isArray(filter) && filter.length !== 0 && filter[0] === 'within'; + return {filter: (globalProperties: GlobalProperties, feature: Feature, canonical?: CanonicalTileID) => compiled.value.evaluate(globalProperties, feature, {}, canonical), + needGeometry}; } } diff --git a/src/style-spec/package.json b/src/style-spec/package.json index 1254536e866..2a18082a312 100644 --- a/src/style-spec/package.json +++ b/src/style-spec/package.json @@ -30,6 +30,7 @@ "dependencies": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", "@mapbox/unitbezier": "^0.0.0", + "@mapbox/point-geometry": "^0.1.0", "csscolorparser": "~1.0.2", "json-stringify-pretty-compact": "^2.0.0", "minimist": "0.0.8", diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json index 587838ef31e..0b87c463a11 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -3454,6 +3454,15 @@ } } }, + "within": { + "doc": "Returns `true` if the feature being evaluated is inside the pre-defined geometry boundary, `false` otherwise. The expression has one argument which must be a valid GeoJSON Polygon/Multi-Polygon object. The expression only evaluates on `Point` or `LineString` feature. For `Point` feature, The expression will return false if any point of the feature is on the boundary or outside the boundary. For `LineString` feature, the expression will return false if the line is fully outside the boundary, or the line is partially intersecting the boundary, which means either part of the line is outside of the boundary, or end point of the line lies on the boundary.", + "group": "Decision", + "sdk-support": { + "basic functionality": { + "js": "1.9.0" + } + } + }, "is-supported-script": { "doc": "Returns `true` if the input string is expected to render legibly. Returns `false` if the input string contains sections that cannot be rendered without potential loss of meaning (e.g. Indic scripts that require complex text shaping, or right-to-left scripts if the the `mapbox-gl-rtl-text` plugin is not in use in Mapbox GL JS).", "group": "String", diff --git a/src/style/properties.js b/src/style/properties.js index 99311fc78d7..1f6f971ec0f 100644 --- a/src/style/properties.js +++ b/src/style/properties.js @@ -1,7 +1,6 @@ // @flow import assert from 'assert'; - import {clone, extend, easeCubicInOut} from '../util/util'; import * as interpolate from '../style-spec/util/interpolate'; import {normalizePropertyExpression} from '../style-spec/expression'; @@ -9,6 +8,7 @@ import Color from '../style-spec/util/color'; import {register} from '../util/web_worker_transfer'; import EvaluationParameters from './evaluation_parameters'; +import type {CanonicalTileID} from '../source/tile_id'; import type {StylePropertySpecification} from '../style-spec/style-spec'; import type { TransitionSpecification, @@ -67,7 +67,7 @@ export type CrossFaded = { */ export interface Property { specification: StylePropertySpecification; - possiblyEvaluate(value: PropertyValue, parameters: EvaluationParameters, availableImages?: Array): R; + possiblyEvaluate(value: PropertyValue, parameters: EvaluationParameters, canonical?: CanonicalTileID, availableImages?: Array): R; interpolate(a: R, b: R, t: number): R; } @@ -105,8 +105,8 @@ export class PropertyValue { return this.expression.kind === 'source' || this.expression.kind === 'composite'; } - possiblyEvaluate(parameters: EvaluationParameters, availableImages?: Array): R { - return this.property.possiblyEvaluate(this, parameters, availableImages); + possiblyEvaluate(parameters: EvaluationParameters, canonical?: CanonicalTileID, availableImages?: Array): R { + return this.property.possiblyEvaluate(this, parameters, canonical, availableImages); } } @@ -264,9 +264,9 @@ class TransitioningPropertyValue { } } - possiblyEvaluate(parameters: EvaluationParameters, availableImages: Array): R { + possiblyEvaluate(parameters: EvaluationParameters, canonical: CanonicalTileID, availableImages: Array): R { const now = parameters.now || 0; - const finalValue = this.value.possiblyEvaluate(parameters, availableImages); + const finalValue = this.value.possiblyEvaluate(parameters, canonical, availableImages); const prior = this.prior; if (!prior) { // No prior value. @@ -283,11 +283,11 @@ class TransitioningPropertyValue { return finalValue; } else if (now < this.begin) { // Transition hasn't started yet. - return prior.possiblyEvaluate(parameters, availableImages); + return prior.possiblyEvaluate(parameters, canonical, availableImages); } else { // Interpolate between recursively-calculated prior value and final. const t = (now - this.begin) / (this.end - this.begin); - return this.property.interpolate(prior.possiblyEvaluate(parameters, availableImages), finalValue, easeCubicInOut(t)); + return this.property.interpolate(prior.possiblyEvaluate(parameters, canonical, availableImages), finalValue, easeCubicInOut(t)); } } } @@ -453,8 +453,8 @@ export class PossiblyEvaluatedPropertyValue { } } - evaluate(feature: Feature, featureState: FeatureState, availableImages?: Array): T { - return this.property.evaluate(this.value, this.parameters, feature, featureState, availableImages); + evaluate(feature: Feature, featureState: FeatureState, canonical?: CanonicalTileID, availableImages?: Array): T { + return this.property.evaluate(this.value, this.parameters, feature, featureState, canonical, availableImages); } } @@ -542,9 +542,9 @@ export class DataDrivenProperty implements Property>, parameters: EvaluationParameters, availableImages?: Array): PossiblyEvaluatedPropertyValue { + possiblyEvaluate(value: PropertyValue>, parameters: EvaluationParameters, canonical?: CanonicalTileID, availableImages?: Array): PossiblyEvaluatedPropertyValue { if (value.expression.kind === 'constant' || value.expression.kind === 'camera') { - return new PossiblyEvaluatedPropertyValue(this, {kind: 'constant', value: value.expression.evaluate(parameters, (null: any), {}, availableImages)}, parameters); + return new PossiblyEvaluatedPropertyValue(this, {kind: 'constant', value: value.expression.evaluate(parameters, (null: any), {}, canonical, availableImages)}, parameters); } else { return new PossiblyEvaluatedPropertyValue(this, value.expression, parameters); } @@ -577,11 +577,11 @@ export class DataDrivenProperty implements Property, parameters: EvaluationParameters, feature: Feature, featureState: FeatureState, availableImages?: Array): T { + evaluate(value: PossiblyEvaluatedValue, parameters: EvaluationParameters, feature: Feature, featureState: FeatureState, canonical?: CanonicalTileID, availableImages?: Array): T { if (value.kind === 'constant') { return value.value; } else { - return value.evaluate(parameters, feature, featureState, availableImages); + return value.evaluate(parameters, feature, featureState, canonical, availableImages); } } } @@ -595,11 +595,11 @@ export class DataDrivenProperty implements Property extends DataDrivenProperty> { - possiblyEvaluate(value: PropertyValue, PossiblyEvaluatedPropertyValue>>, parameters: EvaluationParameters, availableImages?: Array): PossiblyEvaluatedPropertyValue> { + possiblyEvaluate(value: PropertyValue, PossiblyEvaluatedPropertyValue>>, parameters: EvaluationParameters, canonical?: CanonicalTileID, availableImages?: Array): PossiblyEvaluatedPropertyValue> { if (value.value === undefined) { return new PossiblyEvaluatedPropertyValue(this, {kind: 'constant', value: undefined}, parameters); } else if (value.expression.kind === 'constant') { - const evaluatedValue = value.expression.evaluate(parameters, (null: any), {}, availableImages); + const evaluatedValue = value.expression.evaluate(parameters, (null: any), {}, canonical, availableImages); const isImageExpression = value.property.specification.type === 'resolvedImage'; const constantValue = isImageExpression && typeof evaluatedValue !== 'string' ? evaluatedValue.name : evaluatedValue; const constant = this._calculate(constantValue, constantValue, constantValue, parameters); @@ -617,9 +617,9 @@ export class CrossFadedDataDrivenProperty extends DataDrivenProperty>, globals: EvaluationParameters, feature: Feature, featureState: FeatureState, availableImages?: Array): ?CrossFaded { + evaluate(value: PossiblyEvaluatedValue>, globals: EvaluationParameters, feature: Feature, featureState: FeatureState, canonical?: CanonicalTileID, availableImages?: Array): ?CrossFaded { if (value.kind === 'source') { - const constant = value.evaluate(globals, feature, featureState, availableImages); + const constant = value.evaluate(globals, feature, featureState, canonical, availableImages); return this._calculate(constant, constant, constant, globals); } else if (value.kind === 'composite') { return this._calculate( @@ -654,11 +654,11 @@ export class CrossFadedProperty implements Property> { this.specification = specification; } - possiblyEvaluate(value: PropertyValue>, parameters: EvaluationParameters, availableImages?: Array): ?CrossFaded { + possiblyEvaluate(value: PropertyValue>, parameters: EvaluationParameters, canonical?: CanonicalTileID, availableImages?: Array): ?CrossFaded { if (value.value === undefined) { return undefined; } else if (value.expression.kind === 'constant') { - const constant = value.expression.evaluate(parameters, (null: any), {}, availableImages); + const constant = value.expression.evaluate(parameters, (null: any), {}, canonical, availableImages); return this._calculate(constant, constant, constant, parameters); } else { assert(!value.isDataDriven()); @@ -695,8 +695,8 @@ export class ColorRampProperty implements Property { this.specification = specification; } - possiblyEvaluate(value: PropertyValue, parameters: EvaluationParameters, availableImages?: Array): boolean { - return !!value.expression.evaluate(parameters, (null: any), {}, availableImages); + possiblyEvaluate(value: PropertyValue, parameters: EvaluationParameters, canonical?: CanonicalTileID, availableImages?: Array): boolean { + return !!value.expression.evaluate(parameters, (null: any), {}, canonical, availableImages); } interpolate(): boolean { return false; } diff --git a/src/style/style_layer.js b/src/style/style_layer.js index 37ce4c510d4..82c3f68df4d 100644 --- a/src/style/style_layer.js +++ b/src/style/style_layer.js @@ -69,7 +69,7 @@ class StyleLayer extends Evented { this.id = layer.id; this.type = layer.type; - this._featureFilter = () => true; + this._featureFilter = {filter: () => true, needGeometry: false}; if (layer.type === 'custom') return; diff --git a/src/style/style_layer/symbol_style_layer.js b/src/style/style_layer/symbol_style_layer.js index 2963e6aca95..249f0f4b878 100644 --- a/src/style/style_layer/symbol_style_layer.js +++ b/src/style/style_layer/symbol_style_layer.js @@ -29,6 +29,7 @@ import type EvaluationParameters from '../evaluation_parameters'; import type {LayerSpecification} from '../../style-spec/types'; import type {Feature, SourceExpression, CompositeExpression} from '../../style-spec/expression'; import type {Expression} from '../../style-spec/expression/expression'; +import type {CanonicalTileID} from '../../source/tile_id'; import {FormattedType} from '../../style-spec/expression/types'; import {typeOf} from '../../style-spec/expression/values'; import Formatted from '../../style-spec/expression/types/formatted'; @@ -92,8 +93,8 @@ class SymbolStyleLayer extends StyleLayer { this._setPaintOverrides(); } - getValueAndResolveTokens(name: *, feature: Feature, availableImages: Array) { - const value = this.layout.get(name).evaluate(feature, {}, availableImages); + getValueAndResolveTokens(name: *, feature: Feature, canonical: CanonicalTileID, availableImages: Array) { + const value = this.layout.get(name).evaluate(feature, {}, canonical, availableImages); const unevaluated = this._unevaluatedLayout._values[name]; if (!unevaluated.isDataDriven() && !isExpression(unevaluated.value) && value) { return resolveTokens(feature.properties, value); diff --git a/src/symbol/symbol_layout.js b/src/symbol/symbol_layout.js index f3f5fec0d62..9dec812e28d 100644 --- a/src/symbol/symbol_layout.js +++ b/src/symbol/symbol_layout.js @@ -19,7 +19,7 @@ import SymbolBucket from '../data/bucket/symbol_bucket'; import EvaluationParameters from '../style/evaluation_parameters'; import {SIZE_PACK_FACTOR} from './symbol_size'; import ONE_EM from './one_em'; - +import type {CanonicalTileID} from '../source/tile_id'; import type {Shaping, PositionedIcon, TextJustify} from './shaping'; import type {CollisionBoxArray} from '../data/array_types'; import type {SymbolFeature} from '../data/bucket/symbol_bucket'; @@ -148,11 +148,12 @@ export function evaluateVariableOffset(anchor: TextAnchor, offset: [number, numb } export function performSymbolLayout(bucket: SymbolBucket, - glyphMap: {[_: string]: {[_: number]: ?StyleGlyph}}, - glyphPositions: {[_: string]: {[_: number]: GlyphPosition}}, + glyphMap: {[_: string]: {[number]: ?StyleGlyph}}, + glyphPositions: {[_: string]: {[number]: GlyphPosition}}, imageMap: {[_: string]: StyleImage}, imagePositions: {[_: string]: ImagePosition}, - showCollisionBoxes: boolean) { + showCollisionBoxes: boolean, + canonical: CanonicalTileID) { bucket.createArrays(); const tileSize = 512 * bucket.overscaling; @@ -168,21 +169,21 @@ export function performSymbolLayout(bucket: SymbolBucket, if (bucket.textSizeData.kind === 'composite') { const {minZoom, maxZoom} = bucket.textSizeData; sizes.compositeTextSizes = [ - unevaluatedLayoutValues['text-size'].possiblyEvaluate(new EvaluationParameters(minZoom)), - unevaluatedLayoutValues['text-size'].possiblyEvaluate(new EvaluationParameters(maxZoom)) + unevaluatedLayoutValues['text-size'].possiblyEvaluate(new EvaluationParameters(minZoom), canonical), + unevaluatedLayoutValues['text-size'].possiblyEvaluate(new EvaluationParameters(maxZoom), canonical) ]; } if (bucket.iconSizeData.kind === 'composite') { const {minZoom, maxZoom} = bucket.iconSizeData; sizes.compositeIconSizes = [ - unevaluatedLayoutValues['icon-size'].possiblyEvaluate(new EvaluationParameters(minZoom)), - unevaluatedLayoutValues['icon-size'].possiblyEvaluate(new EvaluationParameters(maxZoom)) + unevaluatedLayoutValues['icon-size'].possiblyEvaluate(new EvaluationParameters(minZoom), canonical), + unevaluatedLayoutValues['icon-size'].possiblyEvaluate(new EvaluationParameters(maxZoom), canonical) ]; } - sizes.layoutTextSize = unevaluatedLayoutValues['text-size'].possiblyEvaluate(new EvaluationParameters(bucket.zoom + 1)); - sizes.layoutIconSize = unevaluatedLayoutValues['icon-size'].possiblyEvaluate(new EvaluationParameters(bucket.zoom + 1)); + sizes.layoutTextSize = unevaluatedLayoutValues['text-size'].possiblyEvaluate(new EvaluationParameters(bucket.zoom + 1), canonical); + sizes.layoutIconSize = unevaluatedLayoutValues['icon-size'].possiblyEvaluate(new EvaluationParameters(bucket.zoom + 1), canonical); sizes.textMaxSize = unevaluatedLayoutValues['text-size'].possiblyEvaluate(new EvaluationParameters(18)); const lineHeight = layout.get('text-line-height') * ONE_EM; @@ -191,10 +192,10 @@ export function performSymbolLayout(bucket: SymbolBucket, const textSize = layout.get('text-size'); for (const feature of bucket.features) { - const fontstack = layout.get('text-font').evaluate(feature, {}).join(','); - const layoutTextSizeThisZoom = textSize.evaluate(feature, {}); - const layoutTextSize = sizes.layoutTextSize.evaluate(feature, {}); - const layoutIconSize = sizes.layoutIconSize.evaluate(feature, {}); + const fontstack = layout.get('text-font').evaluate(feature, {}, canonical).join(','); + const layoutTextSizeThisZoom = textSize.evaluate(feature, {}, canonical); + const layoutTextSize = sizes.layoutTextSize.evaluate(feature, {}, canonical); + const layoutIconSize = sizes.layoutIconSize.evaluate(feature, {}, canonical); const shapedTextOrientations = { horizontal: {}, @@ -204,14 +205,14 @@ export function performSymbolLayout(bucket: SymbolBucket, let textOffset: [number, number] = [0, 0]; if (text) { const unformattedText = text.toString(); - const spacing = layout.get('text-letter-spacing').evaluate(feature, {}) * ONE_EM; + const spacing = layout.get('text-letter-spacing').evaluate(feature, {}, canonical) * ONE_EM; const spacingIfAllowed = allowsLetterSpacing(unformattedText) ? spacing : 0; - const textAnchor = layout.get('text-anchor').evaluate(feature, {}); + const textAnchor = layout.get('text-anchor').evaluate(feature, {}, canonical); const variableTextAnchor = layout.get('text-variable-anchor'); if (!variableTextAnchor) { - const radialOffset = layout.get('text-radial-offset').evaluate(feature, {}); + const radialOffset = layout.get('text-radial-offset').evaluate(feature, {}, canonical); // Layers with variable anchors use the `text-radial-offset` property and the [x, y] offset vector // is calculated at placement time instead of layout time if (radialOffset) { @@ -219,17 +220,17 @@ export function performSymbolLayout(bucket: SymbolBucket, // but doesn't actually specify what happens if you use both. We go with the radial offset. textOffset = evaluateVariableOffset(textAnchor, [radialOffset * ONE_EM, INVALID_TEXT_OFFSET]); } else { - textOffset = (layout.get('text-offset').evaluate(feature, {}).map(t => t * ONE_EM): any); + textOffset = (layout.get('text-offset').evaluate(feature, {}, canonical).map(t => t * ONE_EM): any); } } let textJustify = textAlongLine ? "center" : - layout.get('text-justify').evaluate(feature, {}); + layout.get('text-justify').evaluate(feature, {}, canonical); const symbolPlacement = layout.get('symbol-placement'); const maxWidth = symbolPlacement === 'point' ? - layout.get('text-max-width').evaluate(feature, {}) * ONE_EM : + layout.get('text-max-width').evaluate(feature, {}, canonical) * ONE_EM : 0; const addVerticalShapingForPointLabelIfNeeded = () => { @@ -297,8 +298,8 @@ export function performSymbolLayout(bucket: SymbolBucket, if (image) { shapedIcon = shapeIcon( imagePositions[feature.icon.name], - layout.get('icon-offset').evaluate(feature, {}), - layout.get('icon-anchor').evaluate(feature, {})); + layout.get('icon-offset').evaluate(feature, {}, canonical), + layout.get('icon-anchor').evaluate(feature, {}, canonical)); isSDFIcon = image.sdf; if (bucket.sdfIcons === undefined) { bucket.sdfIcons = image.sdf; @@ -316,7 +317,7 @@ export function performSymbolLayout(bucket: SymbolBucket, const shapedText = getDefaultHorizontalShaping(shapedTextOrientations.horizontal) || shapedTextOrientations.vertical; bucket.iconsInText = shapedText ? shapedText.iconsInText : false; if (shapedText || shapedIcon) { - addFeature(bucket, feature, shapedTextOrientations, shapedIcon, imageMap, sizes, layoutTextSize, layoutIconSize, textOffset, isSDFIcon); + addFeature(bucket, feature, shapedTextOrientations, shapedIcon, imageMap, sizes, layoutTextSize, layoutIconSize, textOffset, isSDFIcon, canonical); } } @@ -356,7 +357,7 @@ function addFeature(bucket: SymbolBucket, layoutTextSize: number, layoutIconSize: number, textOffset: [number, number], - isSDFIcon: boolean) { + isSDFIcon: boolean, canonical: CanonicalTileID) { // To reduce the number of labels that jump around when zooming we need // to use a text-size value that is the same for all zoom levels. // bucket calculates text-size at a high zoom level so that all tiles can @@ -365,9 +366,8 @@ function addFeature(bucket: SymbolBucket, if (textMaxSize === undefined) { textMaxSize = layoutTextSize; } - const layout = bucket.layers[0].layout; - const iconOffset = layout.get('icon-offset').evaluate(feature, {}); + const iconOffset = layout.get('icon-offset').evaluate(feature, {}, canonical); const defaultHorizontalShaping = getDefaultHorizontalShaping(shapedTextOrientations.horizontal); const glyphSize = 24, fontScale = layoutTextSize / glyphSize, @@ -409,7 +409,7 @@ function addFeature(bucket: SymbolBucket, bucket.collisionBoxArray, feature.index, feature.sourceLayerIndex, bucket.index, textBoxScale, textPadding, textAlongLine, textOffset, iconBoxScale, iconPadding, iconAlongLine, iconOffset, - feature, sizes, isSDFIcon); + feature, sizes, isSDFIcon, canonical); }; if (symbolPlacement === 'line') { @@ -486,7 +486,8 @@ function addTextVertices(bucket: SymbolBucket, placementTypes: Array<'vertical' | 'center' | 'left' | 'right'>, placedTextSymbolIndices: {[_: string]: number}, placedIconIndex: number, - sizes: Sizes) { + sizes: Sizes, + canonical: CanonicalTileID) { const glyphQuads = getGlyphQuads(anchor, shapedText, textOffset, layer, textAlongLine, feature, imageMap, bucket.allowVerticalPlacement); @@ -502,8 +503,8 @@ function addTextVertices(bucket: SymbolBucket, } } else if (sizeData.kind === 'composite') { textSizeData = [ - SIZE_PACK_FACTOR * sizes.compositeTextSizes[0].evaluate(feature, {}), - SIZE_PACK_FACTOR * sizes.compositeTextSizes[1].evaluate(feature, {}) + SIZE_PACK_FACTOR * sizes.compositeTextSizes[0].evaluate(feature, {}, canonical), + SIZE_PACK_FACTOR * sizes.compositeTextSizes[1].evaluate(feature, {}, canonical) ]; if (textSizeData[0] > MAX_PACKED_SIZE || textSizeData[1] > MAX_PACKED_SIZE) { warnOnce(`${bucket.layerIds[0]}: Value for "text-size" is >= ${MAX_GLYPH_ICON_SIZE}. Reduce your "text-size".`); @@ -521,7 +522,8 @@ function addTextVertices(bucket: SymbolBucket, anchor, lineArray.lineStartIndex, lineArray.lineLength, - placedIconIndex); + placedIconIndex, + canonical); // The placedSymbolArray is used at render time in drawTileSymbols // These indices allow access to the array at collision detection time @@ -568,7 +570,8 @@ function addSymbol(bucket: SymbolBucket, iconOffset: [number, number], feature: SymbolFeature, sizes: Sizes, - isSDFIcon: boolean) { + isSDFIcon: boolean, + canonical: CanonicalTileID) { const lineArray = bucket.addToLineVertexArray(anchor, line); let textCollisionFeature, iconCollisionFeature, verticalTextCollisionFeature, verticalIconCollisionFeature; @@ -585,14 +588,14 @@ function addSymbol(bucket: SymbolBucket, let textOffset0 = 0; let textOffset1 = 0; if (layer._unevaluatedLayout.getValue('text-radial-offset') === undefined) { - [textOffset0, textOffset1] = (layer.layout.get('text-offset').evaluate(feature, {}).map(t => t * ONE_EM): any); + [textOffset0, textOffset1] = (layer.layout.get('text-offset').evaluate(feature, {}, canonical).map(t => t * ONE_EM): any); } else { - textOffset0 = layer.layout.get('text-radial-offset').evaluate(feature, {}) * ONE_EM; + textOffset0 = layer.layout.get('text-radial-offset').evaluate(feature, {}, canonical) * ONE_EM; textOffset1 = INVALID_TEXT_OFFSET; } if (bucket.allowVerticalPlacement && shapedTextOrientations.vertical) { - const textRotation = layer.layout.get('text-rotate').evaluate(feature, {}); + const textRotation = layer.layout.get('text-rotate').evaluate(feature, {}, canonical); const verticalTextRotation = textRotation + 90.0; const verticalShaping = shapedTextOrientations.vertical; verticalTextCollisionFeature = new CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, verticalShaping, textBoxScale, textPadding, textAlongLine, bucket.overscaling, verticalTextRotation); @@ -627,8 +630,8 @@ function addSymbol(bucket: SymbolBucket, } } else if (sizeData.kind === 'composite') { iconSizeData = [ - SIZE_PACK_FACTOR * sizes.compositeIconSizes[0].evaluate(feature, {}), - SIZE_PACK_FACTOR * sizes.compositeIconSizes[1].evaluate(feature, {}) + SIZE_PACK_FACTOR * sizes.compositeIconSizes[0].evaluate(feature, {}, canonical), + SIZE_PACK_FACTOR * sizes.compositeIconSizes[1].evaluate(feature, {}, canonical) ]; if (iconSizeData[0] > MAX_PACKED_SIZE || iconSizeData[1] > MAX_PACKED_SIZE) { warnOnce(`${bucket.layerIds[0]}: Value for "icon-size" is >= ${MAX_GLYPH_ICON_SIZE}. Reduce your "icon-size".`); @@ -647,7 +650,7 @@ function addSymbol(bucket: SymbolBucket, lineArray.lineStartIndex, lineArray.lineLength, // The icon itself does not have an associated symbol since the text isnt placed yet - -1); + -1, canonical); placedIconSymbolIndex = bucket.icon.placedSymbolArray.length - 1; @@ -666,7 +669,7 @@ function addSymbol(bucket: SymbolBucket, lineArray.lineStartIndex, lineArray.lineLength, // The icon itself does not have an associated symbol since the text isnt placed yet - -1); + -1, canonical); verticalPlacedIconSymbolIndex = bucket.icon.placedSymbolArray.length - 1; } @@ -677,7 +680,7 @@ function addSymbol(bucket: SymbolBucket, if (!textCollisionFeature) { key = murmur3(shaping.text); - const textRotate = layer.layout.get('text-rotate').evaluate(feature, {}); + const textRotate = layer.layout.get('text-rotate').evaluate(feature, {}, canonical); // As a collision approximation, we can use either the vertical or any of the horizontal versions of the feature // We're counting on all versions having similar dimensions textCollisionFeature = new CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, shaping, textBoxScale, textPadding, textAlongLine, bucket.overscaling, textRotate); @@ -688,7 +691,7 @@ function addSymbol(bucket: SymbolBucket, bucket, anchor, shaping, imageMap, layer, textAlongLine, feature, textOffset, lineArray, shapedTextOrientations.vertical ? WritingMode.horizontal : WritingMode.horizontalOnly, singleLine ? (Object.keys(shapedTextOrientations.horizontal): any) : [justification], - placedTextSymbolIndices, placedIconSymbolIndex, sizes); + placedTextSymbolIndices, placedIconSymbolIndex, sizes, canonical); if (singleLine) { break; @@ -698,7 +701,7 @@ function addSymbol(bucket: SymbolBucket, if (shapedTextOrientations.vertical) { numVerticalGlyphVertices += addTextVertices( bucket, anchor, shapedTextOrientations.vertical, imageMap, layer, textAlongLine, feature, - textOffset, lineArray, WritingMode.vertical, ['vertical'], placedTextSymbolIndices, verticalPlacedIconSymbolIndex, sizes); + textOffset, lineArray, WritingMode.vertical, ['vertical'], placedTextSymbolIndices, verticalPlacedIconSymbolIndex, sizes, canonical); } const textBoxStartIndex = textCollisionFeature ? textCollisionFeature.boxStartIndex : bucket.collisionBoxArray.length; diff --git a/test/expression.test.js b/test/expression.test.js index ad46463f72a..4465ca27886 100644 --- a/test/expression.test.js +++ b/test/expression.test.js @@ -4,6 +4,57 @@ import {isFunction} from '../src/style-spec/function'; import convertFunction from '../src/style-spec/function/convert'; import {toString} from '../src/style-spec/expression/types'; import ignores from './ignores.json'; +import {CanonicalTileID} from '../src/source/tile_id'; +import MercatorCoordinate from '../src/geo/mercator_coordinate'; + +function convertPoint(coord, canonical, out) { + const p = canonical.getTilePoint(MercatorCoordinate.fromLngLat({lng: coord[0], lat: coord[1]}, 0)); + out.push([p]); +} + +function convertPoints(coords, canonical, out) { + for (let i = 0; i < coords.length; i++) { + convertPoint(coords[i], canonical, out); + } +} + +function convertLines(lines, canonical, out) { + for (let i = 0; i < lines.length; i++) { + const geom = []; + const ring = lines[i]; + for (let j = 0; j < ring.length; j++) { + convertPoint(ring[j], canonical, geom); + } + out.push(geom); + } +} + +function getGeometry(feature, geometry, canonical) { + if (geometry.coordinates) { + const coords = geometry.coordinates; + const type = geometry.type; + feature.type = type; + feature.geometry = []; + if (type === 'Point') { + convertPoint(coords, canonical, feature.geometry); + } else if (type === 'MultiPoint') { + convertPoints(coords, canonical, feature.geometry); + } else if (type === 'LineString') { + convertPoints(coords, canonical, feature.geometry); + } else if (type === 'MultiLineString') { + convertLines(coords, canonical, feature.geometry); + } else if (type === 'Polygon') { + convertLines(coords, canonical, feature.geometry); + + } else if (type === 'MultiPolygon') { + for (let i = 0; i < coords.length; i++) { + const polygon = []; + convertLines(coords[i], canonical, polygon); + feature.geometry.push(polygon); + } + } + } +} let tests; @@ -14,6 +65,7 @@ if (process.argv[1] === __filename && process.argv.length > 2) { run('js', {ignores, tests}, (fixture) => { const spec = Object.assign({}, fixture.propertySpec); let availableImages; + let canonical; if (!spec['property-type']) { spec['property-type'] = 'data-driven'; @@ -50,14 +102,26 @@ run('js', {ignores, tests}, (fixture) => { try { const feature = {properties: input[1].properties || {}}; availableImages = input[0].availableImages || []; + if ('canonicalID' in input[0]) { + const id = input[0].canonicalID; + canonical = new CanonicalTileID(id.z, id.x, id.y); + } else { + canonical = null; + } if ('id' in input[1]) { feature.id = input[1].id; } if ('geometry' in input[1]) { - feature.type = input[1].geometry.type; + if (canonical !== null) { + getGeometry(feature, input[1].geometry, canonical); + } else { + feature.type = input[1].geometry.type; + } } - let value = expression.evaluateWithoutErrorHandling(input[0], feature, {}, availableImages); + + let value = expression.evaluateWithoutErrorHandling(input[0], feature, {}, canonical, availableImages); + if (type.kind === 'color') { value = [value.r, value.g, value.b, value.a]; } diff --git a/test/ignores.json b/test/ignores.json index 2af526d52c3..025342d3e2f 100644 --- a/test/ignores.json +++ b/test/ignores.json @@ -22,14 +22,5 @@ "render-tests/text-size/zero": "https://github.com/mapbox/mapbox-gl-js/issues/9161", "render-tests/text-variable-anchor/left-top-right-bottom-offset-tile-map-mode": "skip - mapbox-gl-js does not support tile-mode", "render-tests/tile-mode/streets-v11": "skip - mapbox-gl-js does not support tile-mode", - "render-tests/within/filter-with-inlined-geojson": "skip - port https://github.com/mapbox/mapbox-gl-native/pull/16157", - "render-tests/within/paint-circle": "skip - port https://github.com/mapbox/mapbox-gl-native/pull/16157", - "render-tests/within/paint-icon": "skip - port https://github.com/mapbox/mapbox-gl-native/pull/16157", - "render-tests/within/paint-text": "skip - port https://github.com/mapbox/mapbox-gl-native/pull/16157", - "render-tests/within/layout-text": "skip - port https://github.com/mapbox/mapbox-gl-native/pull/16194", - "render-tests/within/paint-line": "skip - port https://github.com/mapbox/mapbox-gl-native/pull/16220", - "expression-tests/within/point-within-polygon": "skip - port https://github.com/mapbox/mapbox-gl-native/pull/16157", - "expression-tests/within/invalid-geojson": "skip - port https://github.com/mapbox/mapbox-gl-native/pull/16157", - "expression-tests/within/non-supported": "skip - port https://github.com/mapbox/mapbox-gl-native/pull/16157", - "expression-tests/within/line-within-polygon": "skip - port https://github.com/mapbox/mapbox-gl-native/pull/16220" + "render-tests/within/paint-line": "https://github.com/mapbox/mapbox-gl-js/issues/7023" } diff --git a/test/integration/expression-tests/within/invalid-geojson/test.json b/test/integration/expression-tests/within/invalid-geojson/test.json index 5a6ccfa38fb..bd944da7d9f 100644 --- a/test/integration/expression-tests/within/invalid-geojson/test.json +++ b/test/integration/expression-tests/within/invalid-geojson/test.json @@ -44,7 +44,7 @@ "compiled": { "errors": [{ "key": "", - "error": "'within' expression requires valid geojson source that contains polygon geometry type." + "error": "'within' expression requires valid geojson object that contains polygon geometry type." }], "result": "error" } diff --git a/test/integration/render-tests/within/filter-with-inlined-geojson/expected.png b/test/integration/render-tests/within/filter-with-inlined-geojson/expected.png index 61b6f1796b7..7f3f172cee2 100644 Binary files a/test/integration/render-tests/within/filter-with-inlined-geojson/expected.png and b/test/integration/render-tests/within/filter-with-inlined-geojson/expected.png differ diff --git a/test/integration/render-tests/within/filter-with-inlined-geojson/style.json b/test/integration/render-tests/within/filter-with-inlined-geojson/style.json index 19b184eec78..be4e8fefdd5 100644 --- a/test/integration/render-tests/within/filter-with-inlined-geojson/style.json +++ b/test/integration/render-tests/within/filter-with-inlined-geojson/style.json @@ -7,7 +7,7 @@ } }, "zoom": 3, - "center": [3, 3], + "center": [2.5, 2.5], "sources": { "points": { "type": "geojson", diff --git a/test/integration/render-tests/within/layout-text/expected.png b/test/integration/render-tests/within/layout-text/expected.png index 1fdeb343485..ea25b5a0a40 100644 Binary files a/test/integration/render-tests/within/layout-text/expected.png and b/test/integration/render-tests/within/layout-text/expected.png differ diff --git a/test/integration/render-tests/within/layout-text/style.json b/test/integration/render-tests/within/layout-text/style.json index f12044e5fdc..f1d5655ca17 100644 --- a/test/integration/render-tests/within/layout-text/style.json +++ b/test/integration/render-tests/within/layout-text/style.json @@ -7,7 +7,7 @@ } }, "zoom": 2, - "center": [3.5, 3.5], + "center": [3.05, 3.25], "sources": { "points": { "type": "geojson", diff --git a/test/integration/render-tests/within/paint-circle/expected.png b/test/integration/render-tests/within/paint-circle/expected.png index 2be9f737ca4..8906ad136fb 100644 Binary files a/test/integration/render-tests/within/paint-circle/expected.png and b/test/integration/render-tests/within/paint-circle/expected.png differ diff --git a/test/integration/render-tests/within/paint-circle/style.json b/test/integration/render-tests/within/paint-circle/style.json index e393ce0032f..c7f27d00766 100644 --- a/test/integration/render-tests/within/paint-circle/style.json +++ b/test/integration/render-tests/within/paint-circle/style.json @@ -7,7 +7,7 @@ } }, "zoom": 2, - "center": [3.5, 3.5], + "center": [3.25, 3.25], "sources": { "points": { "type": "geojson", diff --git a/test/integration/render-tests/within/paint-icon/expected.png b/test/integration/render-tests/within/paint-icon/expected.png index f24c546a9a8..c3a5dec3610 100644 Binary files a/test/integration/render-tests/within/paint-icon/expected.png and b/test/integration/render-tests/within/paint-icon/expected.png differ diff --git a/test/integration/render-tests/within/paint-icon/style.json b/test/integration/render-tests/within/paint-icon/style.json index ca58a1f59cf..6bdeaa21840 100644 --- a/test/integration/render-tests/within/paint-icon/style.json +++ b/test/integration/render-tests/within/paint-icon/style.json @@ -7,7 +7,7 @@ } }, "zoom": 2, - "center": [3.5, 3.5], + "center": [3.25, 3.25], "sources": { "points": { "type": "geojson", diff --git a/test/integration/render-tests/within/paint-line-with-simple-polygon/expected.png b/test/integration/render-tests/within/paint-line-with-simple-polygon/expected.png new file mode 100644 index 00000000000..9bf12be16d4 Binary files /dev/null and b/test/integration/render-tests/within/paint-line-with-simple-polygon/expected.png differ diff --git a/test/integration/render-tests/within/paint-line-with-simple-polygon/style.json b/test/integration/render-tests/within/paint-line-with-simple-polygon/style.json new file mode 100644 index 00000000000..bdd94246bc9 --- /dev/null +++ b/test/integration/render-tests/within/paint-line-with-simple-polygon/style.json @@ -0,0 +1,281 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 300, + "height": 200 + } + }, + "zoom": 3, + "center": [ + -20, + -19.55 + ], + "sources": { + "line": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -25.13671875, + -20.879342971957897 + ], + [ + -24.873046874999996, + -18.22935133838667 + ], + [ + -22.412109375, + -17.72775860985227 + ], + [ + -19.6875, + -17.560246503294888 + ], + [ + -18.80859375, + -15.453680224345835 + ], + [ + -16.787109375, + -15.876809064146757 + ], + [ + -14.853515625, + -18.396230138028812 + ], + [ + -15.99609375, + -20.879342971957897 + ], + [ + -14.326171874999998, + -22.350075806124853 + ], + [ + -12.041015625, + -21.207458730482642 + ], + [ + -12.480468749999998, + -19.062117883514652 + ], + [ + -13.974609375, + -16.8886597873816 + ] + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -23.8623046875, + -22.43134015636061 + ], + [ + -15.99609375, + -23.039297747769726 + ] + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -19.951171875, + -25.045792240303435 + ], + [ + -21.357421875, + -13.410994034321702 + ] + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -25.5322265625, + -25.28443774698303 + ], + [ + -16.435546875, + -25.839449402063185 + ] + ] + } + } + ] + } + }, + "polygon": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -25.3125, + -14.944784875088372 + ], + [ + -28.4765625, + -18.47960905583197 + ], + [ + -28.652343749999996, + -21.453068633086772 + ], + [ + -24.08203125, + -24.367113562651262 + ], + [ + -20.214843749999996, + -20.13847031245114 + ], + [ + -17.75390625, + -19.31114335506464 + ], + [ + -17.05078125, + -24.846565348219734 + ], + [ + -10.72265625, + -24.846565348219734 + ], + [ + -10.8984375, + -18.312810846425442 + ], + [ + -15.468749999999998, + -14.264383087562637 + ], + [ + -25.3125, + -14.944784875088372 + ] + ] + ] + } + } + ] + } + } + }, + "layers": [ + { + "id": "border", + "type": "fill", + "source": "polygon", + "paint": { + "fill-color": "black", + "fill-opacity": 0.5 + } + }, + { + "id": "draw", + "type": "line", + "source": "line", + "paint": { + "line-color": [ + "case", + [ + "within", + { + "type": "Polygon", + "coordinates": [ + [ + [ + -25.3125, + -14.944784875088372 + ], + [ + -28.4765625, + -18.47960905583197 + ], + [ + -28.652343749999996, + -21.453068633086772 + ], + [ + -24.08203125, + -24.367113562651262 + ], + [ + -20.214843749999996, + -20.13847031245114 + ], + [ + -17.75390625, + -19.31114335506464 + ], + [ + -17.05078125, + -24.846565348219734 + ], + [ + -10.72265625, + -24.846565348219734 + ], + [ + -10.8984375, + -18.312810846425442 + ], + [ + -15.468749999999998, + -14.264383087562637 + ], + [ + -25.3125, + -14.944784875088372 + ] + ] + ] + } + ], + "red", + "blue" + ] + } + }, + { + "id": "circle", + "type": "circle", + "source": "line", + "paint": { + "circle-color": "yellow", + "circle-radius": 2 + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/within/paint-text/expected.png b/test/integration/render-tests/within/paint-text/expected.png index c44aed8ae61..d0eeffe0d49 100644 Binary files a/test/integration/render-tests/within/paint-text/expected.png and b/test/integration/render-tests/within/paint-text/expected.png differ diff --git a/test/integration/render-tests/within/paint-text/style.json b/test/integration/render-tests/within/paint-text/style.json index 8749edb1721..09a9571d34f 100644 --- a/test/integration/render-tests/within/paint-text/style.json +++ b/test/integration/render-tests/within/paint-text/style.json @@ -7,7 +7,7 @@ } }, "zoom": 2, - "center": [3.5, 3.5], + "center": [3.25, 3.25], "sources": { "points": { "type": "geojson", diff --git a/test/unit/style-spec/feature_filter.test.js b/test/unit/style-spec/feature_filter.test.js index 03438a70962..d81c9103683 100644 --- a/test/unit/style-spec/feature_filter.test.js +++ b/test/unit/style-spec/feature_filter.test.js @@ -2,10 +2,13 @@ import {test} from '../../util/test'; import {default as createFilter, isExpressionFilter} from '../../../src/style-spec/feature_filter'; import convertFilter from '../../../src/style-spec/feature_filter/convert'; +import Point from '@mapbox/point-geometry'; +import MercatorCoordinate from '../../../src/geo/mercator_coordinate'; +import EXTENT from '../../../src/data/extent'; test('filter', t => { t.test('expression, zoom', (t) => { - const f = createFilter(['>=', ['number', ['get', 'x']], ['zoom']]); + const f = createFilter(['>=', ['number', ['get', 'x']], ['zoom']]).filter; t.equal(f({zoom: 1}, {properties: {x: 0}}), false); t.equal(f({zoom: 1}, {properties: {x: 1.5}}), true); t.equal(f({zoom: 1}, {properties: {x: 2.5}}), true); @@ -17,7 +20,7 @@ test('filter', t => { t.test('expression, compare two properties', (t) => { t.stub(console, 'warn'); - const f = createFilter(['==', ['string', ['get', 'x']], ['string', ['get', 'y']]]); + const f = createFilter(['==', ['string', ['get', 'x']], ['string', ['get', 'y']]]).filter; t.equal(f({zoom: 0}, {properties: {x: 1, y: 1}}), false); t.equal(f({zoom: 0}, {properties: {x: '1', y: '1'}}), true); t.equal(f({zoom: 0}, {properties: {x: 'same', y: 'same'}}), true); @@ -27,12 +30,12 @@ test('filter', t => { }); t.test('expression, collator comparison', (t) => { - const caseSensitive = createFilter(['==', ['string', ['get', 'x']], ['string', ['get', 'y']], ['collator', {'case-sensitive': true}]]); + const caseSensitive = createFilter(['==', ['string', ['get', 'x']], ['string', ['get', 'y']], ['collator', {'case-sensitive': true}]]).filter; t.equal(caseSensitive({zoom: 0}, {properties: {x: 'a', y: 'b'}}), false); t.equal(caseSensitive({zoom: 0}, {properties: {x: 'a', y: 'A'}}), false); t.equal(caseSensitive({zoom: 0}, {properties: {x: 'a', y: 'a'}}), true); - const caseInsensitive = createFilter(['==', ['string', ['get', 'x']], ['string', ['get', 'y']], ['collator', {'case-sensitive': false}]]); + const caseInsensitive = createFilter(['==', ['string', ['get', 'x']], ['string', ['get', 'y']], ['collator', {'case-sensitive': false}]]).filter; t.equal(caseInsensitive({zoom: 0}, {properties: {x: 'a', y: 'b'}}), false); t.equal(caseInsensitive({zoom: 0}, {properties: {x: 'a', y: 'A'}}), true); t.equal(caseInsensitive({zoom: 0}, {properties: {x: 'a', y: 'a'}}), true); @@ -40,14 +43,14 @@ test('filter', t => { }); t.test('expression, any/all', (t) => { - t.equal(createFilter(['all'])(), true); - t.equal(createFilter(['all', true])(), true); - t.equal(createFilter(['all', true, false])(), false); - t.equal(createFilter(['all', true, true])(), true); - t.equal(createFilter(['any'])(), false); - t.equal(createFilter(['any', true])(), true); - t.equal(createFilter(['any', true, false])(), true); - t.equal(createFilter(['any', false, false])(), false); + t.equal(createFilter(['all']).filter(), true); + t.equal(createFilter(['all', true]).filter(), true); + t.equal(createFilter(['all', true, false]).filter(), false); + t.equal(createFilter(['all', true, true]).filter(), true); + t.equal(createFilter(['any']).filter(), false); + t.equal(createFilter(['any', true]).filter(), true); + t.equal(createFilter(['any', true, false]).filter(), true); + t.equal(createFilter(['any', false, false]).filter(), false); t.end(); }); @@ -67,6 +70,26 @@ test('filter', t => { t.end(); }); + t.test('expression, within', (t) => { + const getPointFromLngLat = (lng, lat, canonical) => { + const p = MercatorCoordinate.fromLngLat({lng, lat}, 0); + const tilesAtZoom = Math.pow(2, canonical.z); + return new Point( + (p.x * tilesAtZoom - canonical.x) * EXTENT, + (p.y * tilesAtZoom - canonical.y) * EXTENT); + }; + const withinFilter = createFilter(['within', {'type': 'Polygon', 'coordinates': [[[0, 0], [5, 0], [5, 5], [0, 5], [0, 0]]]}]); + t.equal(withinFilter.needGeometry, true); + const canonical = {z: 3, x: 3, y:3}; + t.equal(withinFilter.filter({zoom: 3}, {type: 1, geometry: [[getPointFromLngLat(2, 2, canonical)]]}, canonical), true); + t.equal(withinFilter.filter({zoom: 3}, {type: 1, geometry: [[getPointFromLngLat(6, 6, canonical)]]}, canonical), false); + t.equal(withinFilter.filter({zoom: 3}, {type: 1, geometry: [[getPointFromLngLat(5, 5, canonical)]]}, canonical), false); + t.equal(withinFilter.filter({zoom: 3}, {type: 2, geometry: [[getPointFromLngLat(2, 2, canonical), getPointFromLngLat(3, 3, canonical)]]}, canonical), true); + t.equal(withinFilter.filter({zoom: 3}, {type: 2, geometry: [[getPointFromLngLat(6, 6, canonical), getPointFromLngLat(2, 2, canonical)]]}, canonical), false); + t.equal(withinFilter.filter({zoom: 3}, {type: 2, geometry: [[getPointFromLngLat(5, 5, canonical), getPointFromLngLat(2, 2, canonical)]]}, canonical), false); + t.end(); + }); + legacyFilterTests(t, createFilter); t.end(); @@ -120,7 +143,7 @@ test('convert legacy filters to expressions', t => { ]; const converted = convertFilter(filter); - const f = createFilter(converted); + const f = createFilter(converted).filter; t.equal(f({zoom: 0}, {properties: {x: 0, y: 1, z: 1}}), true); t.equal(f({zoom: 0}, {properties: {x: 1, y: 0, z: 1}}), true); @@ -197,23 +220,23 @@ test('convert legacy filters to expressions', t => { t.end(); }); -function legacyFilterTests(t, filter) { +function legacyFilterTests(t, createFilterExpr) { t.test('degenerate', (t) => { - t.equal(filter()(), true); - t.equal(filter(undefined)(), true); - t.equal(filter(null)(), true); + t.equal(createFilterExpr().filter(), true); + t.equal(createFilterExpr(undefined).filter(), true); + t.equal(createFilterExpr(null).filter(), true); t.end(); }); t.test('==, string', (t) => { - const f = filter(['==', 'foo', 'bar']); + const f = createFilterExpr(['==', 'foo', 'bar']).filter; t.equal(f({zoom: 0}, {properties: {foo: 'bar'}}), true); t.equal(f({zoom: 0}, {properties: {foo: 'baz'}}), false); t.end(); }); t.test('==, number', (t) => { - const f = filter(['==', 'foo', 0]); + const f = createFilterExpr(['==', 'foo', 0]).filter; t.equal(f({zoom: 0}, {properties: {foo: 0}}), true); t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); t.equal(f({zoom: 0}, {properties: {foo: '0'}}), false); @@ -226,7 +249,7 @@ function legacyFilterTests(t, filter) { }); t.test('==, null', (t) => { - const f = filter(['==', 'foo', null]); + const f = createFilterExpr(['==', 'foo', null]).filter; t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); t.equal(f({zoom: 0}, {properties: {foo: '0'}}), false); @@ -239,14 +262,14 @@ function legacyFilterTests(t, filter) { }); t.test('==, $type', (t) => { - const f = filter(['==', '$type', 'LineString']); + const f = createFilterExpr(['==', '$type', 'LineString']).filter; t.equal(f({zoom: 0}, {type: 1}), false); t.equal(f({zoom: 0}, {type: 2}), true); t.end(); }); t.test('==, $id', (t) => { - const f = filter(['==', '$id', 1234]); + const f = createFilterExpr(['==', '$id', 1234]).filter; t.equal(f({zoom: 0}, {id: 1234}), true); t.equal(f({zoom: 0}, {id: '1234'}), false); @@ -256,14 +279,14 @@ function legacyFilterTests(t, filter) { }); t.test('!=, string', (t) => { - const f = filter(['!=', 'foo', 'bar']); + const f = createFilterExpr(['!=', 'foo', 'bar']).filter; t.equal(f({zoom: 0}, {properties: {foo: 'bar'}}), false); t.equal(f({zoom: 0}, {properties: {foo: 'baz'}}), true); t.end(); }); t.test('!=, number', (t) => { - const f = filter(['!=', 'foo', 0]); + const f = createFilterExpr(['!=', 'foo', 0]).filter; t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); t.equal(f({zoom: 0}, {properties: {foo: 1}}), true); t.equal(f({zoom: 0}, {properties: {foo: '0'}}), true); @@ -276,7 +299,7 @@ function legacyFilterTests(t, filter) { }); t.test('!=, null', (t) => { - const f = filter(['!=', 'foo', null]); + const f = createFilterExpr(['!=', 'foo', null]).filter; t.equal(f({zoom: 0}, {properties: {foo: 0}}), true); t.equal(f({zoom: 0}, {properties: {foo: 1}}), true); t.equal(f({zoom: 0}, {properties: {foo: '0'}}), true); @@ -289,14 +312,14 @@ function legacyFilterTests(t, filter) { }); t.test('!=, $type', (t) => { - const f = filter(['!=', '$type', 'LineString']); + const f = createFilterExpr(['!=', '$type', 'LineString']).filter; t.equal(f({zoom: 0}, {type: 1}), true); t.equal(f({zoom: 0}, {type: 2}), false); t.end(); }); t.test('<, number', (t) => { - const f = filter(['<', 'foo', 0]); + const f = createFilterExpr(['<', 'foo', 0]).filter; t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); t.equal(f({zoom: 0}, {properties: {foo: -1}}), true); @@ -312,7 +335,7 @@ function legacyFilterTests(t, filter) { }); t.test('<, string', (t) => { - const f = filter(['<', 'foo', '0']); + const f = createFilterExpr(['<', 'foo', '0']).filter; t.equal(f({zoom: 0}, {properties: {foo: -1}}), false); t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); @@ -327,7 +350,7 @@ function legacyFilterTests(t, filter) { }); t.test('<=, number', (t) => { - const f = filter(['<=', 'foo', 0]); + const f = createFilterExpr(['<=', 'foo', 0]).filter; t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); t.equal(f({zoom: 0}, {properties: {foo: 0}}), true); t.equal(f({zoom: 0}, {properties: {foo: -1}}), true); @@ -343,7 +366,7 @@ function legacyFilterTests(t, filter) { }); t.test('<=, string', (t) => { - const f = filter(['<=', 'foo', '0']); + const f = createFilterExpr(['<=', 'foo', '0']).filter; t.equal(f({zoom: 0}, {properties: {foo: -1}}), false); t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); @@ -358,7 +381,7 @@ function legacyFilterTests(t, filter) { }); t.test('>, number', (t) => { - const f = filter(['>', 'foo', 0]); + const f = createFilterExpr(['>', 'foo', 0]).filter; t.equal(f({zoom: 0}, {properties: {foo: 1}}), true); t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); t.equal(f({zoom: 0}, {properties: {foo: -1}}), false); @@ -374,7 +397,7 @@ function legacyFilterTests(t, filter) { }); t.test('>, string', (t) => { - const f = filter(['>', 'foo', '0']); + const f = createFilterExpr(['>', 'foo', '0']).filter; t.equal(f({zoom: 0}, {properties: {foo: -1}}), false); t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); @@ -389,7 +412,7 @@ function legacyFilterTests(t, filter) { }); t.test('>=, number', (t) => { - const f = filter(['>=', 'foo', 0]); + const f = createFilterExpr(['>=', 'foo', 0]).filter; t.equal(f({zoom: 0}, {properties: {foo: 1}}), true); t.equal(f({zoom: 0}, {properties: {foo: 0}}), true); t.equal(f({zoom: 0}, {properties: {foo: -1}}), false); @@ -405,7 +428,7 @@ function legacyFilterTests(t, filter) { }); t.test('>=, string', (t) => { - const f = filter(['>=', 'foo', '0']); + const f = createFilterExpr(['>=', 'foo', '0']).filter; t.equal(f({zoom: 0}, {properties: {foo: -1}}), false); t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); @@ -420,13 +443,13 @@ function legacyFilterTests(t, filter) { }); t.test('in, degenerate', (t) => { - const f = filter(['in', 'foo']); + const f = createFilterExpr(['in', 'foo']).filter; t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); t.end(); }); t.test('in, string', (t) => { - const f = filter(['in', 'foo', '0']); + const f = createFilterExpr(['in', 'foo', '0']).filter; t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); t.equal(f({zoom: 0}, {properties: {foo: '0'}}), true); t.equal(f({zoom: 0}, {properties: {foo: true}}), false); @@ -438,7 +461,7 @@ function legacyFilterTests(t, filter) { }); t.test('in, number', (t) => { - const f = filter(['in', 'foo', 0]); + const f = createFilterExpr(['in', 'foo', 0]).filter; t.equal(f({zoom: 0}, {properties: {foo: 0}}), true); t.equal(f({zoom: 0}, {properties: {foo: '0'}}), false); t.equal(f({zoom: 0}, {properties: {foo: true}}), false); @@ -449,7 +472,7 @@ function legacyFilterTests(t, filter) { }); t.test('in, null', (t) => { - const f = filter(['in', 'foo', null]); + const f = createFilterExpr(['in', 'foo', null]).filter; t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); t.equal(f({zoom: 0}, {properties: {foo: '0'}}), false); t.equal(f({zoom: 0}, {properties: {foo: true}}), false); @@ -460,7 +483,7 @@ function legacyFilterTests(t, filter) { }); t.test('in, multiple', (t) => { - const f = filter(['in', 'foo', 0, 1]); + const f = createFilterExpr(['in', 'foo', 0, 1]).filter; t.equal(f({zoom: 0}, {properties: {foo: 0}}), true); t.equal(f({zoom: 0}, {properties: {foo: 1}}), true); t.equal(f({zoom: 0}, {properties: {foo: 3}}), false); @@ -470,7 +493,7 @@ function legacyFilterTests(t, filter) { t.test('in, large_multiple', (t) => { const values = Array.from({length: 2000}).map(Number.call, Number); values.reverse(); - const f = filter(['in', 'foo'].concat(values)); + const f = createFilterExpr(['in', 'foo'].concat(values)).filter; t.equal(f({zoom: 0}, {properties: {foo: 0}}), true); t.equal(f({zoom: 0}, {properties: {foo: 1}}), true); t.equal(f({zoom: 0}, {properties: {foo: 1999}}), true); @@ -482,7 +505,7 @@ function legacyFilterTests(t, filter) { const values = Array.from({length: 2000}).map(Number.call, Number); values.push('a'); values.unshift('b'); - const f = filter(['in', 'foo'].concat(values)); + const f = createFilterExpr(['in', 'foo'].concat(values)).filter; t.equal(f({zoom: 0}, {properties: {foo: 'b'}}), true); t.equal(f({zoom: 0}, {properties: {foo: 'a'}}), true); t.equal(f({zoom: 0}, {properties: {foo: 0}}), true); @@ -493,12 +516,12 @@ function legacyFilterTests(t, filter) { }); t.test('in, $type', (t) => { - const f = filter(['in', '$type', 'LineString', 'Polygon']); + const f = createFilterExpr(['in', '$type', 'LineString', 'Polygon']).filter; t.equal(f({zoom: 0}, {type: 1}), false); t.equal(f({zoom: 0}, {type: 2}), true); t.equal(f({zoom: 0}, {type: 3}), true); - const f1 = filter(['in', '$type', 'Polygon', 'LineString', 'Point']); + const f1 = createFilterExpr(['in', '$type', 'Polygon', 'LineString', 'Point']).filter; t.equal(f1({zoom: 0}, {type: 1}), true); t.equal(f1({zoom: 0}, {type: 2}), true); t.equal(f1({zoom: 0}, {type: 3}), true); @@ -507,13 +530,13 @@ function legacyFilterTests(t, filter) { }); t.test('!in, degenerate', (t) => { - const f = filter(['!in', 'foo']); + const f = createFilterExpr(['!in', 'foo']).filter; t.equal(f({zoom: 0}, {properties: {foo: 1}}), true); t.end(); }); t.test('!in, string', (t) => { - const f = filter(['!in', 'foo', '0']); + const f = createFilterExpr(['!in', 'foo', '0']).filter; t.equal(f({zoom: 0}, {properties: {foo: 0}}), true); t.equal(f({zoom: 0}, {properties: {foo: '0'}}), false); t.equal(f({zoom: 0}, {properties: {foo: null}}), true); @@ -523,7 +546,7 @@ function legacyFilterTests(t, filter) { }); t.test('!in, number', (t) => { - const f = filter(['!in', 'foo', 0]); + const f = createFilterExpr(['!in', 'foo', 0]).filter; t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); t.equal(f({zoom: 0}, {properties: {foo: '0'}}), true); t.equal(f({zoom: 0}, {properties: {foo: null}}), true); @@ -532,7 +555,7 @@ function legacyFilterTests(t, filter) { }); t.test('!in, null', (t) => { - const f = filter(['!in', 'foo', null]); + const f = createFilterExpr(['!in', 'foo', null]).filter; t.equal(f({zoom: 0}, {properties: {foo: 0}}), true); t.equal(f({zoom: 0}, {properties: {foo: '0'}}), true); t.equal(f({zoom: 0}, {properties: {foo: null}}), false); @@ -541,7 +564,7 @@ function legacyFilterTests(t, filter) { }); t.test('!in, multiple', (t) => { - const f = filter(['!in', 'foo', 0, 1]); + const f = createFilterExpr(['!in', 'foo', 0, 1]).filter; t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); t.equal(f({zoom: 0}, {properties: {foo: 3}}), true); @@ -549,7 +572,7 @@ function legacyFilterTests(t, filter) { }); t.test('!in, large_multiple', (t) => { - const f = filter(['!in', 'foo'].concat(Array.from({length: 2000}).map(Number.call, Number))); + const f = createFilterExpr(['!in', 'foo'].concat(Array.from({length: 2000}).map(Number.call, Number))).filter; t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); t.equal(f({zoom: 0}, {properties: {foo: 1999}}), false); @@ -558,7 +581,7 @@ function legacyFilterTests(t, filter) { }); t.test('!in, $type', (t) => { - const f = filter(['!in', '$type', 'LineString', 'Polygon']); + const f = createFilterExpr(['!in', '$type', 'LineString', 'Polygon']).filter; t.equal(f({zoom: 0}, {type: 1}), true); t.equal(f({zoom: 0}, {type: 2}), false); t.equal(f({zoom: 0}, {type: 3}), false); @@ -566,55 +589,55 @@ function legacyFilterTests(t, filter) { }); t.test('any', (t) => { - const f1 = filter(['any']); + const f1 = createFilterExpr(['any']).filter; t.equal(f1({zoom: 0}, {properties: {foo: 1}}), false); - const f2 = filter(['any', ['==', 'foo', 1]]); + const f2 = createFilterExpr(['any', ['==', 'foo', 1]]).filter; t.equal(f2({zoom: 0}, {properties: {foo: 1}}), true); - const f3 = filter(['any', ['==', 'foo', 0]]); + const f3 = createFilterExpr(['any', ['==', 'foo', 0]]).filter; t.equal(f3({zoom: 0}, {properties: {foo: 1}}), false); - const f4 = filter(['any', ['==', 'foo', 0], ['==', 'foo', 1]]); + const f4 = createFilterExpr(['any', ['==', 'foo', 0], ['==', 'foo', 1]]).filter; t.equal(f4({zoom: 0}, {properties: {foo: 1}}), true); t.end(); }); t.test('all', (t) => { - const f1 = filter(['all']); + const f1 = createFilterExpr(['all']).filter; t.equal(f1({zoom: 0}, {properties: {foo: 1}}), true); - const f2 = filter(['all', ['==', 'foo', 1]]); + const f2 = createFilterExpr(['all', ['==', 'foo', 1]]).filter; t.equal(f2({zoom: 0}, {properties: {foo: 1}}), true); - const f3 = filter(['all', ['==', 'foo', 0]]); + const f3 = createFilterExpr(['all', ['==', 'foo', 0]]).filter; t.equal(f3({zoom: 0}, {properties: {foo: 1}}), false); - const f4 = filter(['all', ['==', 'foo', 0], ['==', 'foo', 1]]); + const f4 = createFilterExpr(['all', ['==', 'foo', 0], ['==', 'foo', 1]]).filter; t.equal(f4({zoom: 0}, {properties: {foo: 1}}), false); t.end(); }); t.test('none', (t) => { - const f1 = filter(['none']); + const f1 = createFilterExpr(['none']).filter; t.equal(f1({zoom: 0}, {properties: {foo: 1}}), true); - const f2 = filter(['none', ['==', 'foo', 1]]); + const f2 = createFilterExpr(['none', ['==', 'foo', 1]]).filter; t.equal(f2({zoom: 0}, {properties: {foo: 1}}), false); - const f3 = filter(['none', ['==', 'foo', 0]]); + const f3 = createFilterExpr(['none', ['==', 'foo', 0]]).filter; t.equal(f3({zoom: 0}, {properties: {foo: 1}}), true); - const f4 = filter(['none', ['==', 'foo', 0], ['==', 'foo', 1]]); + const f4 = createFilterExpr(['none', ['==', 'foo', 0], ['==', 'foo', 1]]).filter; t.equal(f4({zoom: 0}, {properties: {foo: 1}}), false); t.end(); }); t.test('has', (t) => { - const f = filter(['has', 'foo']); + const f = createFilterExpr(['has', 'foo']).filter; t.equal(f({zoom: 0}, {properties: {foo: 0}}), true); t.equal(f({zoom: 0}, {properties: {foo: 1}}), true); t.equal(f({zoom: 0}, {properties: {foo: '0'}}), true); @@ -627,7 +650,7 @@ function legacyFilterTests(t, filter) { }); t.test('!has', (t) => { - const f = filter(['!has', 'foo']); + const f = createFilterExpr(['!has', 'foo']).filter; t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); t.equal(f({zoom: 0}, {properties: {foo: '0'}}), false);